├── .github ├── dependabot.yml └── workflows │ ├── audit-on-push.yml │ ├── docker.yml │ ├── general.yml │ ├── publish_binaries.yml │ └── scheduled-audit.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── docker ├── Dockerfile └── README.md ├── src ├── lib.rs ├── main.rs ├── recipe.rs └── skeleton │ ├── mod.rs │ ├── read.rs │ ├── target.rs │ └── version_masking.rs └── tests ├── recipe.rs └── skeletons.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | ignore: 9 | - dependency-name: "*" 10 | update-types: 11 | - "version-update:semver-patch" 12 | -------------------------------------------------------------------------------- /.github/workflows/audit-on-push.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | push: 4 | paths: 5 | - '**/Cargo.toml' 6 | - '**/Cargo.lock' 7 | 8 | permissions: 9 | issues: write 10 | checks: write 11 | 12 | jobs: 13 | security_audit: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: rustsec/audit-check@v1.4.1 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker images 2 | on: 3 | push: 4 | branches: [main] 5 | schedule: 6 | - cron: '42 7 * * *' # run at 7:42 UTC (morning) every day 7 | 8 | permissions: 9 | contents: read 10 | 11 | env: 12 | RUST_IMAGE_TAG: latest 13 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} 14 | DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} 15 | DOCKER_REPO: ${{ secrets.DOCKERHUB_USERNAME }}/cargo-chef 16 | 17 | jobs: 18 | rust_image_tag_matrix: 19 | name: Generate Rust Docker image tag matrix 20 | runs-on: ubuntu-latest 21 | outputs: 22 | matrix: ${{ steps.set-matrix.outputs.matrix }} 23 | steps: 24 | - 25 | id: set-matrix 26 | run: | 27 | (echo -n 'matrix=[' \ 28 | && curl --silent https://raw.githubusercontent.com/docker-library/official-images/master/library/rust \ 29 | | grep -E Tags: \ 30 | | cut -d ' ' -f 2- \ 31 | | sed 's/, /\n/g' \ 32 | | sed 's/\(.*\)/"\1",/g' \ 33 | | tr '\n' ' ' \ 34 | | sed '$ s/..$//' \ 35 | && echo ']') >> "$GITHUB_OUTPUT" 36 | build_and_push: 37 | name: Build and push 38 | needs: [rust_image_tag_matrix] 39 | runs-on: ubuntu-latest 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | rust_image_tag: ${{fromJSON(needs.rust_image_tag_matrix.outputs.matrix)}} 44 | steps: 45 | - 46 | name: Checkout 47 | uses: actions/checkout@v4 48 | - 49 | name: Set up QEMU 50 | uses: docker/setup-qemu-action@v3 51 | - 52 | name: Set up Docker Buildx 53 | uses: docker/setup-buildx-action@v3 54 | - 55 | name: Login to DockerHub 56 | uses: docker/login-action@v3 57 | with: 58 | username: ${{ secrets.DOCKERHUB_USERNAME }} 59 | password: ${{ secrets.DOCKERHUB_TOKEN }} 60 | - 61 | # Get package version from git tags 62 | name: Get package version 63 | id: package_version 64 | run: |- 65 | git fetch --tags 66 | VER=$(git tag --sort="-v:refname" | head -n 1 | cut -d"v" -f2) 67 | echo "result=$VER" >> "$GITHUB_OUTPUT" 68 | - 69 | # Check if version matches ^\d+\.\d+\.\d+$ 70 | name: Determine if release version 71 | id: is_release_version 72 | run: | 73 | if [[ ${{ steps.package_version.outputs.result }} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 74 | echo "result=true" >> "$GITHUB_OUTPUT" 75 | fi 76 | - 77 | name: Determine if duplicated 78 | id: is_duplicated 79 | run: | 80 | CHEF_PACKAGE_VERSION=${{ steps.package_version.outputs.result }} 81 | CHEF_IMAGE_TAG=$CHEF_PACKAGE_VERSION-rust-$RUST_IMAGE_TAG 82 | CHEF_IMAGE=$DOCKER_REPO:$CHEF_IMAGE_TAG 83 | 84 | if DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect $CHEF_IMAGE >/dev/null; then 85 | echo "There is already a pushed image with ${CHEF_IMAGE_TAG} as tag. Skipping." 86 | echo "result=true" >> "$GITHUB_OUTPUT" 87 | else 88 | echo "result=false" >> "$GITHUB_OUTPUT" 89 | fi 90 | - 91 | name: Build and push without `latest` tag 92 | if: ${{ steps.is_release_version.outputs.result == 'true' && steps.is_duplicated.outputs.result == 'false' && matrix.rust_image_tag != 'latest' }} 93 | run: | 94 | RUST_IMAGE_TAG=${{ matrix.rust_image_tag }} 95 | CHEF_PACKAGE_VERSION=${{ steps.package_version.outputs.result }} 96 | CHEF_IMAGE=$DOCKER_REPO:$CHEF_PACKAGE_VERSION-rust-$RUST_IMAGE_TAG 97 | CHEF_IMAGE_LATEST=$DOCKER_REPO:latest-rust-$RUST_IMAGE_TAG 98 | 99 | docker buildx build \ 100 | --tag $CHEF_IMAGE \ 101 | --tag $CHEF_IMAGE_LATEST \ 102 | --build-arg=BASE_IMAGE=rust:$RUST_IMAGE_TAG \ 103 | --build-arg=CHEF_TAG=$CHEF_PACKAGE_VERSION \ 104 | --platform linux/amd64,linux/arm64 \ 105 | --push \ 106 | ./docker 107 | - 108 | # Latest Rust version, latest cargo-chef version 109 | name: Build and push with `latest` tag 110 | if: ${{ steps.is_release_version.outputs.result == 'true' && steps.is_duplicated.outputs.result == 'false' && matrix.rust_image_tag == 'latest' }} 111 | run: | 112 | RUST_IMAGE_TAG=${{ matrix.rust_image_tag }} 113 | CHEF_PACKAGE_VERSION=${{ steps.package_version.outputs.result }} 114 | CHEF_IMAGE=$DOCKER_REPO:$CHEF_PACKAGE_VERSION-rust-$RUST_IMAGE_TAG 115 | CHEF_IMAGE_LATEST=$DOCKER_REPO:latest-rust-$RUST_IMAGE_TAG 116 | 117 | docker buildx build \ 118 | --tag $CHEF_IMAGE \ 119 | --tag $CHEF_IMAGE_LATEST \ 120 | --tag $DOCKER_REPO:latest \ 121 | --build-arg=BASE_IMAGE=rust:$RUST_IMAGE_TAG \ 122 | --build-arg=CHEF_TAG=$CHEF_PACKAGE_VERSION \ 123 | --platform linux/amd64,linux/arm64 \ 124 | --push \ 125 | ./docker 126 | -------------------------------------------------------------------------------- /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: dtolnay/rust-toolchain@stable 15 | - run: cargo test 16 | 17 | fmt: 18 | name: Rustfmt 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: dtolnay/rust-toolchain@stable 23 | with: 24 | components: rustfmt 25 | - run: cargo fmt --all -- --check 26 | 27 | clippy: 28 | name: Clippy 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: dtolnay/rust-toolchain@stable 33 | with: 34 | components: clippy 35 | - run: cargo clippy -- -D warnings 36 | 37 | coverage: 38 | name: Code coverage 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | - name: Install stable toolchain 45 | uses: dtolnay/rust-toolchain@stable 46 | - name: Install cargo-tarpaulin 47 | uses: taiki-e/install-action@v2 48 | with: 49 | tool: cargo-tarpaulin 50 | checksum: true 51 | - name: Run cargo-tarpaulin 52 | run: cargo tarpaulin --ignore-tests 53 | -------------------------------------------------------------------------------- /.github/workflows/publish_binaries.yml: -------------------------------------------------------------------------------- 1 | name: Publish Binaries 2 | on: 3 | push: 4 | tags: 5 | - v[0-9]+.[0-9]+.[0-9]+ 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | create-release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: taiki-e/create-gh-release-action@v1 16 | with: 17 | branch: main 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | upload-assets: 22 | name: upload-assets (${{ matrix.os }} ${{ matrix.target }}) 23 | needs: 24 | - create-release 25 | strategy: 26 | matrix: 27 | include: 28 | - target: x86_64-unknown-linux-gnu 29 | os: ubuntu-20.04 30 | - target: x86_64-unknown-linux-musl 31 | os: ubuntu-latest 32 | - target: x86_64-apple-darwin 33 | os: macos-latest 34 | - target: x86_64-pc-windows-msvc 35 | os: windows-latest 36 | runs-on: ${{ matrix.os }} 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: taiki-e/upload-rust-binary-action@v1 40 | with: 41 | bin: cargo-chef 42 | target: ${{ matrix.target }} 43 | tar: unix 44 | zip: windows 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/scheduled-audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | 6 | permissions: 7 | issues: write 8 | checks: write 9 | 10 | jobs: 11 | audit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: rustsec/audit-check@v1.4.1 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | recipe.json -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "anyhow" 66 | version = "1.0.95" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 69 | 70 | [[package]] 71 | name = "assert_cmd" 72 | version = "2.0.16" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 75 | dependencies = [ 76 | "anstyle", 77 | "bstr", 78 | "doc-comment", 79 | "libc", 80 | "predicates", 81 | "predicates-core", 82 | "predicates-tree", 83 | "wait-timeout", 84 | ] 85 | 86 | [[package]] 87 | name = "assert_fs" 88 | version = "1.1.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" 91 | dependencies = [ 92 | "anstyle", 93 | "doc-comment", 94 | "globwalk 0.9.1", 95 | "predicates", 96 | "predicates-core", 97 | "predicates-tree", 98 | "tempfile", 99 | ] 100 | 101 | [[package]] 102 | name = "autocfg" 103 | version = "1.4.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 106 | 107 | [[package]] 108 | name = "bitflags" 109 | version = "1.3.2" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 112 | 113 | [[package]] 114 | name = "bitflags" 115 | version = "2.8.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 118 | 119 | [[package]] 120 | name = "bstr" 121 | version = "1.11.3" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" 124 | dependencies = [ 125 | "memchr", 126 | "regex-automata", 127 | "serde", 128 | ] 129 | 130 | [[package]] 131 | name = "camino" 132 | version = "1.1.9" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 135 | dependencies = [ 136 | "serde", 137 | ] 138 | 139 | [[package]] 140 | name = "cargo-chef" 141 | version = "0.1.71" 142 | dependencies = [ 143 | "anyhow", 144 | "assert_cmd", 145 | "assert_fs", 146 | "cargo-manifest", 147 | "cargo_metadata", 148 | "clap", 149 | "env_logger", 150 | "expect-test", 151 | "fs-err", 152 | "globwalk 0.8.1", 153 | "log", 154 | "pathdiff", 155 | "predicates", 156 | "serde", 157 | "serde_json", 158 | "toml", 159 | ] 160 | 161 | [[package]] 162 | name = "cargo-manifest" 163 | version = "0.18.1" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "be15dc781a9a07129a03f2ff1491d9b5d5936beeba1db95b8f1da6544b0eff2c" 166 | dependencies = [ 167 | "serde", 168 | "thiserror 2.0.11", 169 | "toml", 170 | ] 171 | 172 | [[package]] 173 | name = "cargo-platform" 174 | version = "0.1.9" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" 177 | dependencies = [ 178 | "serde", 179 | ] 180 | 181 | [[package]] 182 | name = "cargo_metadata" 183 | version = "0.15.4" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" 186 | dependencies = [ 187 | "camino", 188 | "cargo-platform", 189 | "semver", 190 | "serde", 191 | "serde_json", 192 | "thiserror 1.0.69", 193 | ] 194 | 195 | [[package]] 196 | name = "cfg-if" 197 | version = "1.0.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 200 | 201 | [[package]] 202 | name = "clap" 203 | version = "4.5.27" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" 206 | dependencies = [ 207 | "clap_builder", 208 | "clap_derive", 209 | ] 210 | 211 | [[package]] 212 | name = "clap_builder" 213 | version = "4.5.27" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" 216 | dependencies = [ 217 | "anstream", 218 | "anstyle", 219 | "clap_lex", 220 | "strsim", 221 | ] 222 | 223 | [[package]] 224 | name = "clap_derive" 225 | version = "4.5.24" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" 228 | dependencies = [ 229 | "heck", 230 | "proc-macro2", 231 | "quote", 232 | "syn", 233 | ] 234 | 235 | [[package]] 236 | name = "clap_lex" 237 | version = "0.7.4" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 240 | 241 | [[package]] 242 | name = "colorchoice" 243 | version = "1.0.3" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 246 | 247 | [[package]] 248 | name = "crossbeam-deque" 249 | version = "0.8.6" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 252 | dependencies = [ 253 | "crossbeam-epoch", 254 | "crossbeam-utils", 255 | ] 256 | 257 | [[package]] 258 | name = "crossbeam-epoch" 259 | version = "0.9.18" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 262 | dependencies = [ 263 | "crossbeam-utils", 264 | ] 265 | 266 | [[package]] 267 | name = "crossbeam-utils" 268 | version = "0.8.21" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 271 | 272 | [[package]] 273 | name = "difflib" 274 | version = "0.4.0" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 277 | 278 | [[package]] 279 | name = "dissimilar" 280 | version = "1.0.9" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" 283 | 284 | [[package]] 285 | name = "doc-comment" 286 | version = "0.3.3" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 289 | 290 | [[package]] 291 | name = "env_logger" 292 | version = "0.10.2" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 295 | dependencies = [ 296 | "humantime", 297 | "is-terminal", 298 | "log", 299 | "regex", 300 | "termcolor", 301 | ] 302 | 303 | [[package]] 304 | name = "equivalent" 305 | version = "1.0.1" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 308 | 309 | [[package]] 310 | name = "errno" 311 | version = "0.3.10" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 314 | dependencies = [ 315 | "libc", 316 | "windows-sys", 317 | ] 318 | 319 | [[package]] 320 | name = "expect-test" 321 | version = "1.5.1" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0" 324 | dependencies = [ 325 | "dissimilar", 326 | "once_cell", 327 | ] 328 | 329 | [[package]] 330 | name = "fastrand" 331 | version = "2.3.0" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 334 | 335 | [[package]] 336 | name = "float-cmp" 337 | version = "0.10.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" 340 | dependencies = [ 341 | "num-traits", 342 | ] 343 | 344 | [[package]] 345 | name = "fs-err" 346 | version = "2.11.0" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" 349 | dependencies = [ 350 | "autocfg", 351 | ] 352 | 353 | [[package]] 354 | name = "getrandom" 355 | version = "0.2.15" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 358 | dependencies = [ 359 | "cfg-if", 360 | "libc", 361 | "wasi", 362 | ] 363 | 364 | [[package]] 365 | name = "globset" 366 | version = "0.4.15" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" 369 | dependencies = [ 370 | "aho-corasick", 371 | "bstr", 372 | "log", 373 | "regex-automata", 374 | "regex-syntax", 375 | ] 376 | 377 | [[package]] 378 | name = "globwalk" 379 | version = "0.8.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" 382 | dependencies = [ 383 | "bitflags 1.3.2", 384 | "ignore", 385 | "walkdir", 386 | ] 387 | 388 | [[package]] 389 | name = "globwalk" 390 | version = "0.9.1" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" 393 | dependencies = [ 394 | "bitflags 2.8.0", 395 | "ignore", 396 | "walkdir", 397 | ] 398 | 399 | [[package]] 400 | name = "hashbrown" 401 | version = "0.15.2" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 404 | 405 | [[package]] 406 | name = "heck" 407 | version = "0.5.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 410 | 411 | [[package]] 412 | name = "hermit-abi" 413 | version = "0.4.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 416 | 417 | [[package]] 418 | name = "humantime" 419 | version = "2.1.0" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 422 | 423 | [[package]] 424 | name = "ignore" 425 | version = "0.4.23" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" 428 | dependencies = [ 429 | "crossbeam-deque", 430 | "globset", 431 | "log", 432 | "memchr", 433 | "regex-automata", 434 | "same-file", 435 | "walkdir", 436 | "winapi-util", 437 | ] 438 | 439 | [[package]] 440 | name = "indexmap" 441 | version = "2.7.1" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 444 | dependencies = [ 445 | "equivalent", 446 | "hashbrown", 447 | ] 448 | 449 | [[package]] 450 | name = "is-terminal" 451 | version = "0.4.15" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" 454 | dependencies = [ 455 | "hermit-abi", 456 | "libc", 457 | "windows-sys", 458 | ] 459 | 460 | [[package]] 461 | name = "is_terminal_polyfill" 462 | version = "1.70.1" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 465 | 466 | [[package]] 467 | name = "itoa" 468 | version = "1.0.14" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 471 | 472 | [[package]] 473 | name = "libc" 474 | version = "0.2.169" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 477 | 478 | [[package]] 479 | name = "linux-raw-sys" 480 | version = "0.4.15" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 483 | 484 | [[package]] 485 | name = "log" 486 | version = "0.4.25" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 489 | 490 | [[package]] 491 | name = "memchr" 492 | version = "2.7.4" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 495 | 496 | [[package]] 497 | name = "normalize-line-endings" 498 | version = "0.3.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 501 | 502 | [[package]] 503 | name = "num-traits" 504 | version = "0.2.19" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 507 | dependencies = [ 508 | "autocfg", 509 | ] 510 | 511 | [[package]] 512 | name = "once_cell" 513 | version = "1.20.2" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 516 | 517 | [[package]] 518 | name = "pathdiff" 519 | version = "0.2.3" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 522 | 523 | [[package]] 524 | name = "predicates" 525 | version = "3.1.3" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" 528 | dependencies = [ 529 | "anstyle", 530 | "difflib", 531 | "float-cmp", 532 | "normalize-line-endings", 533 | "predicates-core", 534 | "regex", 535 | ] 536 | 537 | [[package]] 538 | name = "predicates-core" 539 | version = "1.0.9" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" 542 | 543 | [[package]] 544 | name = "predicates-tree" 545 | version = "1.0.12" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" 548 | dependencies = [ 549 | "predicates-core", 550 | "termtree", 551 | ] 552 | 553 | [[package]] 554 | name = "proc-macro2" 555 | version = "1.0.93" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 558 | dependencies = [ 559 | "unicode-ident", 560 | ] 561 | 562 | [[package]] 563 | name = "quote" 564 | version = "1.0.38" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 567 | dependencies = [ 568 | "proc-macro2", 569 | ] 570 | 571 | [[package]] 572 | name = "regex" 573 | version = "1.11.1" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 576 | dependencies = [ 577 | "aho-corasick", 578 | "memchr", 579 | "regex-automata", 580 | "regex-syntax", 581 | ] 582 | 583 | [[package]] 584 | name = "regex-automata" 585 | version = "0.4.9" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 588 | dependencies = [ 589 | "aho-corasick", 590 | "memchr", 591 | "regex-syntax", 592 | ] 593 | 594 | [[package]] 595 | name = "regex-syntax" 596 | version = "0.8.5" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 599 | 600 | [[package]] 601 | name = "rustix" 602 | version = "0.38.44" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 605 | dependencies = [ 606 | "bitflags 2.8.0", 607 | "errno", 608 | "libc", 609 | "linux-raw-sys", 610 | "windows-sys", 611 | ] 612 | 613 | [[package]] 614 | name = "ryu" 615 | version = "1.0.19" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 618 | 619 | [[package]] 620 | name = "same-file" 621 | version = "1.0.6" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 624 | dependencies = [ 625 | "winapi-util", 626 | ] 627 | 628 | [[package]] 629 | name = "semver" 630 | version = "1.0.25" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" 633 | dependencies = [ 634 | "serde", 635 | ] 636 | 637 | [[package]] 638 | name = "serde" 639 | version = "1.0.217" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 642 | dependencies = [ 643 | "serde_derive", 644 | ] 645 | 646 | [[package]] 647 | name = "serde_derive" 648 | version = "1.0.217" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 651 | dependencies = [ 652 | "proc-macro2", 653 | "quote", 654 | "syn", 655 | ] 656 | 657 | [[package]] 658 | name = "serde_json" 659 | version = "1.0.138" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" 662 | dependencies = [ 663 | "itoa", 664 | "memchr", 665 | "ryu", 666 | "serde", 667 | ] 668 | 669 | [[package]] 670 | name = "serde_spanned" 671 | version = "0.6.8" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 674 | dependencies = [ 675 | "serde", 676 | ] 677 | 678 | [[package]] 679 | name = "strsim" 680 | version = "0.11.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 683 | 684 | [[package]] 685 | name = "syn" 686 | version = "2.0.96" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 689 | dependencies = [ 690 | "proc-macro2", 691 | "quote", 692 | "unicode-ident", 693 | ] 694 | 695 | [[package]] 696 | name = "tempfile" 697 | version = "3.15.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" 700 | dependencies = [ 701 | "cfg-if", 702 | "fastrand", 703 | "getrandom", 704 | "once_cell", 705 | "rustix", 706 | "windows-sys", 707 | ] 708 | 709 | [[package]] 710 | name = "termcolor" 711 | version = "1.4.1" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 714 | dependencies = [ 715 | "winapi-util", 716 | ] 717 | 718 | [[package]] 719 | name = "termtree" 720 | version = "0.5.1" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" 723 | 724 | [[package]] 725 | name = "thiserror" 726 | version = "1.0.69" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 729 | dependencies = [ 730 | "thiserror-impl 1.0.69", 731 | ] 732 | 733 | [[package]] 734 | name = "thiserror" 735 | version = "2.0.11" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 738 | dependencies = [ 739 | "thiserror-impl 2.0.11", 740 | ] 741 | 742 | [[package]] 743 | name = "thiserror-impl" 744 | version = "1.0.69" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 747 | dependencies = [ 748 | "proc-macro2", 749 | "quote", 750 | "syn", 751 | ] 752 | 753 | [[package]] 754 | name = "thiserror-impl" 755 | version = "2.0.11" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 758 | dependencies = [ 759 | "proc-macro2", 760 | "quote", 761 | "syn", 762 | ] 763 | 764 | [[package]] 765 | name = "toml" 766 | version = "0.8.19" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 769 | dependencies = [ 770 | "indexmap", 771 | "serde", 772 | "serde_spanned", 773 | "toml_datetime", 774 | "toml_edit", 775 | ] 776 | 777 | [[package]] 778 | name = "toml_datetime" 779 | version = "0.6.8" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 782 | dependencies = [ 783 | "serde", 784 | ] 785 | 786 | [[package]] 787 | name = "toml_edit" 788 | version = "0.22.22" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 791 | dependencies = [ 792 | "indexmap", 793 | "serde", 794 | "serde_spanned", 795 | "toml_datetime", 796 | "winnow", 797 | ] 798 | 799 | [[package]] 800 | name = "unicode-ident" 801 | version = "1.0.16" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" 804 | 805 | [[package]] 806 | name = "utf8parse" 807 | version = "0.2.2" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 810 | 811 | [[package]] 812 | name = "wait-timeout" 813 | version = "0.2.0" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 816 | dependencies = [ 817 | "libc", 818 | ] 819 | 820 | [[package]] 821 | name = "walkdir" 822 | version = "2.5.0" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 825 | dependencies = [ 826 | "same-file", 827 | "winapi-util", 828 | ] 829 | 830 | [[package]] 831 | name = "wasi" 832 | version = "0.11.0+wasi-snapshot-preview1" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 835 | 836 | [[package]] 837 | name = "winapi-util" 838 | version = "0.1.9" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 841 | dependencies = [ 842 | "windows-sys", 843 | ] 844 | 845 | [[package]] 846 | name = "windows-sys" 847 | version = "0.59.0" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 850 | dependencies = [ 851 | "windows-targets", 852 | ] 853 | 854 | [[package]] 855 | name = "windows-targets" 856 | version = "0.52.6" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 859 | dependencies = [ 860 | "windows_aarch64_gnullvm", 861 | "windows_aarch64_msvc", 862 | "windows_i686_gnu", 863 | "windows_i686_gnullvm", 864 | "windows_i686_msvc", 865 | "windows_x86_64_gnu", 866 | "windows_x86_64_gnullvm", 867 | "windows_x86_64_msvc", 868 | ] 869 | 870 | [[package]] 871 | name = "windows_aarch64_gnullvm" 872 | version = "0.52.6" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 875 | 876 | [[package]] 877 | name = "windows_aarch64_msvc" 878 | version = "0.52.6" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 881 | 882 | [[package]] 883 | name = "windows_i686_gnu" 884 | version = "0.52.6" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 887 | 888 | [[package]] 889 | name = "windows_i686_gnullvm" 890 | version = "0.52.6" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 893 | 894 | [[package]] 895 | name = "windows_i686_msvc" 896 | version = "0.52.6" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 899 | 900 | [[package]] 901 | name = "windows_x86_64_gnu" 902 | version = "0.52.6" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 905 | 906 | [[package]] 907 | name = "windows_x86_64_gnullvm" 908 | version = "0.52.6" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 911 | 912 | [[package]] 913 | name = "windows_x86_64_msvc" 914 | version = "0.52.6" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 917 | 918 | [[package]] 919 | name = "winnow" 920 | version = "0.6.25" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310" 923 | dependencies = [ 924 | "memchr", 925 | ] 926 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-chef" 3 | version = "0.1.71" 4 | authors = ["Luca Palmieri "] 5 | edition = "2018" 6 | description = "A cargo sub-command to build project dependencies for optimal Docker layer caching." 7 | keywords = ["cargo", "docker", "caching", "dependencies"] 8 | categories = ["development-tools::cargo-plugins", "command-line-utilities"] 9 | repository = "https://github.com/LukeMathWalker/cargo-chef" 10 | documentation = "https://docs.rs/cargo-chef" 11 | license = "Apache-2.0 OR MIT" 12 | exclude = ["tests"] 13 | 14 | [[bin]] 15 | name = "cargo-chef" 16 | path = "src/main.rs" 17 | 18 | [lib] 19 | name = "chef" 20 | path = "src/lib.rs" 21 | 22 | [dependencies] 23 | clap = { version = "4", features = ["cargo", "env", "derive"] } 24 | serde = { version = "1.0.117", features = ["derive"] } 25 | serde_json = "1.0.59" 26 | log = "0.4.11" 27 | env_logger = "0.10" 28 | globwalk = "0.8.0" 29 | anyhow = "1.0.33" 30 | pathdiff = "0.2.0" 31 | cargo-manifest = "0.18.1" 32 | fs-err = "2.5.0" 33 | toml = { version = "0.8", features = ["preserve_order"] } 34 | expect-test = "1.1.0" 35 | cargo_metadata = "0.15" 36 | 37 | [dev-dependencies] 38 | assert_cmd = "2" 39 | assert_fs = "1.0.0" 40 | predicates = "3" 41 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luca Palmieri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

cargo-chef

2 |
3 | 4 | Cache the dependencies of your Rust project and speed up your Docker builds. 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | 13 | Crates.io version 15 | 16 | 17 | 18 | Download 20 | 21 |
22 |
23 | 24 | > `cargo-chef` was initially developed for the deployment chapter of [Zero to Production In Rust](https://zero2prod.com), a hands-on introduction to backend development using the Rust programming language. 25 | 26 | # Table of Contents 27 | 0. [How to install](#how-to-install) 28 | 1. [How to use](#how-to-use) 29 | 2. [Benefits vs Limitations](#benefits-vs-limitations) 30 | 3. [License](#license) 31 | 32 | ## How To Install 33 | 34 | You can install `cargo-chef` from [crates.io](https://crates.io) with 35 | 36 | ```bash 37 | cargo install cargo-chef --locked 38 | ``` 39 | 40 | ## How to use 41 | 42 | > :warning: **cargo-chef is not meant to be run locally** 43 | > Its primary use-case is to speed up container builds by running BEFORE 44 | > the actual source code is copied over. Don't run it on existing codebases to avoid 45 | > having files being overwritten. 46 | 47 | `cargo-chef` exposes two commands: `prepare` and `cook`: 48 | 49 | ```bash 50 | cargo chef --help 51 | ``` 52 | 53 | ```text 54 | 55 | cargo-chef 56 | 57 | USAGE: 58 | cargo chef 59 | 60 | SUBCOMMANDS: 61 | cook Re-hydrate the minimum project skeleton identified by `cargo chef prepare` and 62 | build it to cache dependencies 63 | prepare Analyze the current project to determine the minimum subset of files (Cargo.lock 64 | and Cargo.toml manifests) required to build it and cache dependencies 65 | ``` 66 | 67 | `prepare` examines your project and builds a _recipe_ that captures the set of information required to build your dependencies. 68 | 69 | ```bash 70 | cargo chef prepare --recipe-path recipe.json 71 | ``` 72 | 73 | Nothing too mysterious going on here, you can examine the `recipe.json` file: it contains the skeleton of your project (e.g. all the `Cargo.toml` files with their relative path, the `Cargo.lock` file is available) plus a few additional pieces of information. 74 | In particular it makes sure that all libraries and binaries are explicitly declared in their respective `Cargo.toml` files even if they can be found at the canonical default location (`src/main.rs` for a binary, `src/lib.rs` for a library). 75 | 76 | The `recipe.json` is the equivalent of the Python `requirements.txt` file - it is the only input required for `cargo chef cook`, the command that will build out our dependencies: 77 | 78 | ```bash 79 | cargo chef cook --recipe-path recipe.json 80 | ``` 81 | 82 | If you want to build in `--release` mode: 83 | 84 | ```bash 85 | cargo chef cook --release --recipe-path recipe.json 86 | ``` 87 | 88 | You can also choose to override which Rust toolchain should be used. E.g., to force the `nightly` toolchain: 89 | 90 | ```bash 91 | cargo +nightly chef cook --recipe-path recipe.json 92 | ``` 93 | 94 | `cargo-chef` is designed to be leveraged in Dockerfiles: 95 | 96 | ```dockerfile 97 | FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef 98 | WORKDIR /app 99 | 100 | FROM chef AS planner 101 | COPY . . 102 | RUN cargo chef prepare --recipe-path recipe.json 103 | 104 | FROM chef AS builder 105 | COPY --from=planner /app/recipe.json recipe.json 106 | # Build dependencies - this is the caching Docker layer! 107 | RUN cargo chef cook --release --recipe-path recipe.json 108 | # Build application 109 | COPY . . 110 | RUN cargo build --release --bin app 111 | 112 | # We do not need the Rust toolchain to run the binary! 113 | FROM debian:bookworm-slim AS runtime 114 | WORKDIR /app 115 | COPY --from=builder /app/target/release/app /usr/local/bin 116 | ENTRYPOINT ["/usr/local/bin/app"] 117 | ``` 118 | 119 | We are using three stages: the first computes the recipe file, the second caches our dependencies and builds the binary, the third is our runtime environment. 120 | As long as your dependencies do not change the `recipe.json` file will stay the same, therefore the outcome of `cargo chef cook --release --recipe-path recipe.json` will be cached, massively speeding up your builds (up to 5x measured on some commercial projects). 121 | 122 | ### Pre-built images 123 | 124 | We offer `lukemathwalker/cargo-chef` as a pre-built Docker image equipped with both Rust and `cargo-chef`. 125 | 126 | The tagging scheme is `-rust-`. 127 | For example, `0.1.22-rust-1.56.0`. 128 | You can choose to get the latest version of either `cargo-chef` or `rust` by using: 129 | - `latest-rust-1.56.0` (use latest `cargo-chef` with specific Rust version); 130 | - `0.1.22-rust-latest` (use latest Rust with specific `cargo-chef` version). 131 | You can find [all the available tags on Dockerhub](https://hub.docker.com/r/lukemathwalker/cargo-chef). 132 | 133 | > :warning: **You must use the same Rust version in all stages** 134 | > If you use a different Rust version in one of the stages 135 | > caching will not work as expected. 136 | 137 | ### Without the pre-built image 138 | 139 | If you do not want to use the `lukemathwalker/cargo-chef` image, you can simply install the CLI within the Dockerfile: 140 | 141 | ```dockerfile 142 | FROM rust:1 AS chef 143 | # We only pay the installation cost once, 144 | # it will be cached from the second build onwards 145 | RUN cargo install cargo-chef 146 | WORKDIR app 147 | 148 | FROM chef AS planner 149 | COPY . . 150 | RUN cargo chef prepare --recipe-path recipe.json 151 | 152 | FROM chef AS builder 153 | COPY --from=planner /app/recipe.json recipe.json 154 | # Build dependencies - this is the caching Docker layer! 155 | RUN cargo chef cook --release --recipe-path recipe.json 156 | # Build application 157 | COPY . . 158 | RUN cargo build --release --bin app 159 | 160 | # We do not need the Rust toolchain to run the binary! 161 | FROM debian:bookworm-slim AS runtime 162 | WORKDIR app 163 | COPY --from=builder /app/target/release/app /usr/local/bin 164 | ENTRYPOINT ["/usr/local/bin/app"] 165 | ``` 166 | 167 | ### Running the binary in Alpine 168 | 169 | If you want to run your application using the `alpine` distribution you need to create a fully static binary. 170 | The recommended approach is to build for the `x86_64-unknown-linux-musl` target using [`muslrust`](https://github.com/clux/muslrust). 171 | `cargo-chef` works for `x86_64-unknown-linux-musl`, but we are **cross-compiling** - the target 172 | toolchain must be explicitly specified. 173 | 174 | A sample Dockerfile looks like this: 175 | 176 | ```dockerfile 177 | # Using the `rust-musl-builder` as base image, instead of 178 | # the official Rust toolchain 179 | FROM clux/muslrust:stable AS chef 180 | USER root 181 | RUN cargo install cargo-chef 182 | WORKDIR /app 183 | 184 | FROM chef AS planner 185 | COPY . . 186 | RUN cargo chef prepare --recipe-path recipe.json 187 | 188 | FROM chef AS builder 189 | COPY --from=planner /app/recipe.json recipe.json 190 | # Notice that we are specifying the --target flag! 191 | RUN cargo chef cook --release --target x86_64-unknown-linux-musl --recipe-path recipe.json 192 | COPY . . 193 | RUN cargo build --release --target x86_64-unknown-linux-musl --bin app 194 | 195 | FROM alpine AS runtime 196 | RUN addgroup -S myuser && adduser -S myuser -G myuser 197 | COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /usr/local/bin/ 198 | USER myuser 199 | CMD ["/usr/local/bin/app"] 200 | ``` 201 | 202 | ## Benefits vs Limitations 203 | 204 | `cargo-chef` has been tested on a few OpenSource projects and some of commercial projects, but our testing has definitely not exhausted the range of possibilities when it comes to `cargo build` customisations and we are sure that there are a few rough edges that will have to be smoothed out - please file issues on [GitHub](https://github.com/LukeMathWalker/cargo-chef). 205 | 206 | ### Benefits of `cargo-chef`: 207 | 208 | A common alternative is to load a minimal `main.rs` into a container with `Cargo.toml` and `Cargo.lock` to build a Docker layer that consists of only your dependencies ([more info here](https://www.lpalmieri.com/posts/fast-rust-docker-builds/#caching-rust-builds)). This is fragile compared to `cargo-chef` which will instead: 209 | 210 | - automatically pick up all crates in a workspace (and new ones as they are added) 211 | - keep working when files or crates are moved around, which would instead require manual edits to the `Dockerfile` using the "manual" approach 212 | - generate fewer intermediate Docker layers (for workspaces) 213 | 214 | ### Limitations and caveats: 215 | 216 | - `cargo chef cook` and `cargo build` must be executed from the same working directory. If you examine the `*.d` files under `target/debug/deps` for one of your projects using `cat` you will notice that they contain absolute paths referring to the project `target` directory. If moved around, `cargo` will not leverage them as cached dependencies; 217 | - `cargo build` will build local dependencies (outside of the current project) from scratch, even if they are unchanged, due to the reliance of its fingerprinting logic on timestamps (see [this _long_ issue on `cargo`'s repository](https://github.com/rust-lang/cargo/issues/2644)); 218 | 219 | ## License 220 | 221 | Licensed under either of Apache License, Version 2.0 or MIT license at your option. 222 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 223 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=rust 2 | FROM $BASE_IMAGE 3 | ARG CHEF_TAG 4 | ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse 5 | 6 | # Install musl-dev on Alpine to avoid error "ld: cannot find crti.o: No such file or directory" 7 | RUN ((cat /etc/os-release | grep ID | grep alpine) && apk add --no-cache musl-dev || true) \ 8 | && cargo install cargo-chef --locked --version $CHEF_TAG \ 9 | && rm -rf $CARGO_HOME/registry/ 10 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a Dockerfile to build a `cargo-chef` image that can be used as 2 | a base layer for `planner` and `builder` stages in your Dockerfiles. -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod recipe; 2 | mod skeleton; 3 | 4 | pub use recipe::{ 5 | AllFeatures, CommandArg, CookArgs, DefaultFeatures, OptimisationProfile, Recipe, TargetArgs, 6 | }; 7 | pub use skeleton::*; 8 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context}; 2 | use chef::{ 3 | AllFeatures, CommandArg, CookArgs, DefaultFeatures, OptimisationProfile, Recipe, TargetArgs, 4 | }; 5 | use clap::crate_version; 6 | use clap::Parser; 7 | use fs_err as fs; 8 | use std::collections::HashSet; 9 | use std::io::IsTerminal; 10 | use std::path::PathBuf; 11 | 12 | /// Cache the dependencies of your Rust project. 13 | #[derive(Parser)] 14 | #[command( 15 | bin_name = "cargo", 16 | version = crate_version!(), 17 | author = "Luca Palmieri " 18 | )] 19 | pub struct Cli { 20 | #[command(subcommand)] 21 | command: CargoInvocation, 22 | } 23 | 24 | #[derive(Parser)] 25 | pub enum CargoInvocation { 26 | // All `cargo` subcommands receive their name (e.g. `chef` as the first command). 27 | // See https://github.com/rust-lang/rustfmt/pull/3569 28 | Chef { 29 | #[command(subcommand)] 30 | command: Command, 31 | }, 32 | } 33 | 34 | #[derive(Parser)] 35 | #[command( 36 | version = crate_version!(), 37 | author = "Luca Palmieri " 38 | )] 39 | pub enum Command { 40 | /// Analyze the current project to determine the minimum subset of files (Cargo.lock and 41 | /// Cargo.toml manifests) required to build it and cache dependencies. 42 | /// 43 | /// `cargo chef prepare` emits a recipe file that can be later used via 44 | /// `cargo chef cook --recipe .json`. 45 | Prepare(Prepare), 46 | /// Re-hydrate the minimum project skeleton identified by `cargo chef prepare` and build 47 | /// it to cache dependencies. 48 | Cook(Cook), 49 | } 50 | 51 | #[derive(Parser)] 52 | pub struct Prepare { 53 | /// The filepath used to save the computed recipe. 54 | /// 55 | /// It defaults to "recipe.json". 56 | #[arg(long, default_value = "recipe.json")] 57 | recipe_path: PathBuf, 58 | 59 | /// When --bin is specified, `cargo-chef` will ignore all members of the workspace 60 | /// that are not necessary to successfully compile the specific binary. 61 | #[arg(long)] 62 | bin: Option, 63 | } 64 | 65 | #[derive(Parser)] 66 | pub struct Cook { 67 | /// The filepath `cook` should be reading the recipe from. 68 | /// 69 | /// It defaults to "recipe.json". 70 | #[arg(long, default_value = "recipe.json")] 71 | recipe_path: PathBuf, 72 | /// Build artifacts with the specified profile. 73 | #[arg(long)] 74 | profile: Option, 75 | /// Build in release mode. 76 | #[arg(long)] 77 | release: bool, 78 | /// Run `cargo check` instead of `cargo build`. Primarily useful for speeding up your CI pipeline. 79 | #[arg(long)] 80 | check: bool, 81 | /// Run `cargo clippy` instead of `cargo build`. Primarily useful for speeding up your CI pipeline. Requires clippy to be installed. 82 | #[arg(long)] 83 | clippy: bool, 84 | /// Build for the target triple. The flag can be passed multiple times to cook for multiple targets. 85 | #[arg(long)] 86 | target: Option>, 87 | /// Directory for all generated artifacts. 88 | #[arg(long, env = "CARGO_TARGET_DIR")] 89 | target_dir: Option, 90 | /// Do not activate the `default` feature. 91 | #[arg(long)] 92 | no_default_features: bool, 93 | /// Enable all features. 94 | #[arg(long)] 95 | all_features: bool, 96 | /// Space or comma separated list of features to activate. 97 | #[arg(long, value_delimiter = ',')] 98 | features: Option>, 99 | /// Unstable feature to activate (only available on the nightly channel). 100 | #[arg(short = 'Z')] 101 | unstable_features: Option>, 102 | /// Build all benches 103 | #[arg(long)] 104 | benches: bool, 105 | /// Build all tests 106 | #[arg(long)] 107 | tests: bool, 108 | /// Build all examples 109 | #[arg(long)] 110 | examples: bool, 111 | /// Build all targets. 112 | /// This is equivalent to specifying `--tests --benches --examples`. 113 | #[arg(long)] 114 | all_targets: bool, 115 | /// Path to Cargo.toml 116 | #[arg(long)] 117 | manifest_path: Option, 118 | /// Package(s) to build (see `cargo help pkgid`) 119 | #[arg(long, short = 'p')] 120 | package: Option>, 121 | /// Build all members in the workspace. 122 | #[arg(long)] 123 | workspace: bool, 124 | /// Build offline. 125 | #[arg(long)] 126 | offline: bool, 127 | /// Require Cargo.lock is up to date 128 | #[arg(long)] 129 | locked: bool, 130 | /// Use verbose output 131 | #[arg(long, short = 'v')] 132 | verbose: bool, 133 | /// Require Cargo.lock and cache are up to date 134 | #[arg(long)] 135 | frozen: bool, 136 | /// Report build timings. 137 | #[arg(long)] 138 | timings: bool, 139 | /// Cook using `#[no_std]` configuration (does not affect `proc-macro` crates) 140 | #[arg(long)] 141 | no_std: bool, 142 | /// Build only the specified binary. This can be specified with multiple binaries. 143 | #[arg(long)] 144 | bin: Option>, 145 | /// Build all binaries and ignore everything else. 146 | #[arg(long)] 147 | bins: bool, 148 | /// Run `cargo zigbuild` instead of `cargo build`. You need to install 149 | /// the `cargo-zigbuild` crate and the Zig compiler toolchain separately 150 | #[arg(long)] 151 | zigbuild: bool, 152 | /// Modify the current workspace to maximise cache reuse, but don't invoke `cargo build`. 153 | /// This option exist to leverage `cargo-chef` when trying to cache dependencies in Rust 154 | /// projects that rely on a custom build system (i.e. not `cargo`). 155 | #[clap(long)] 156 | no_build: bool, 157 | } 158 | 159 | fn _main() -> Result<(), anyhow::Error> { 160 | let current_directory = std::env::current_dir().unwrap(); 161 | 162 | let cli = Cli::parse(); 163 | // "Unwrapping" the actual command. 164 | let command = match cli.command { 165 | CargoInvocation::Chef { command } => command, 166 | }; 167 | 168 | match command { 169 | Command::Cook(Cook { 170 | recipe_path, 171 | profile, 172 | release, 173 | check, 174 | clippy, 175 | target, 176 | no_default_features, 177 | all_features, 178 | features, 179 | unstable_features, 180 | target_dir, 181 | benches, 182 | tests, 183 | examples, 184 | all_targets, 185 | manifest_path, 186 | package, 187 | workspace, 188 | offline, 189 | frozen, 190 | locked, 191 | verbose, 192 | timings, 193 | no_std, 194 | bin, 195 | zigbuild, 196 | bins, 197 | no_build, 198 | }) => { 199 | if std::io::stdout().is_terminal() { 200 | eprintln!("WARNING stdout appears to be a terminal."); 201 | eprintln!( 202 | "cargo-chef is not meant to be run in an interactive environment \ 203 | and will overwrite some existing files (namely any `lib.rs`, `main.rs` and \ 204 | `Cargo.toml` it finds)." 205 | ); 206 | eprintln!(); 207 | eprint!("To continue anyway, type `yes`: "); 208 | 209 | let mut answer = String::with_capacity(3); 210 | std::io::stdin() 211 | .read_line(&mut answer) 212 | .context("Failed to read from stdin")?; 213 | 214 | if "yes" != answer.trim() { 215 | std::process::exit(1); 216 | } 217 | } 218 | 219 | let features: Option> = features.and_then(|features| { 220 | if features.is_empty() { 221 | None 222 | } else { 223 | Some(features.into_iter().collect()) 224 | } 225 | }); 226 | 227 | let unstable_features: Option> = 228 | unstable_features.and_then(|unstable_features| { 229 | if unstable_features.is_empty() { 230 | None 231 | } else { 232 | Some(unstable_features.into_iter().collect()) 233 | } 234 | }); 235 | 236 | let profile = match (release, profile) { 237 | (false, None) => OptimisationProfile::Debug, 238 | (false, Some(profile)) if profile == "dev" => OptimisationProfile::Debug, 239 | (true, None) => OptimisationProfile::Release, 240 | (false, Some(profile)) if profile == "release" => OptimisationProfile::Release, 241 | (false, Some(custom_profile)) => OptimisationProfile::Other(custom_profile), 242 | (true, Some(_)) => Err(anyhow!("You specified both --release and --profile arguments. Please remove one of them, or both"))? 243 | }; 244 | let command = match (check, clippy, zigbuild, no_build) { 245 | (true, false, false, false) => CommandArg::Check, 246 | (false, true, false, false) => CommandArg::Clippy, 247 | (false, false, true, false) => CommandArg::Zigbuild, 248 | (false, false, false, true) => CommandArg::NoBuild, 249 | (false, false, false, false) => CommandArg::Build, 250 | _ => Err(anyhow!("Only one (or none) of the `clippy`, `check`, `zigbuild`, and `no-build` arguments are allowed. Please remove some of them, or all"))?, 251 | }; 252 | 253 | let default_features = if no_default_features { 254 | DefaultFeatures::Disabled 255 | } else { 256 | DefaultFeatures::Enabled 257 | }; 258 | 259 | let all_features = if all_features { 260 | AllFeatures::Enabled 261 | } else { 262 | AllFeatures::Disabled 263 | }; 264 | 265 | let serialized = fs::read_to_string(recipe_path) 266 | .context("Failed to read recipe from the specified path.")?; 267 | let recipe: Recipe = 268 | serde_json::from_str(&serialized).context("Failed to deserialize recipe.")?; 269 | let target_args = TargetArgs { 270 | benches, 271 | tests, 272 | examples, 273 | all_targets, 274 | }; 275 | recipe 276 | .cook(CookArgs { 277 | profile, 278 | command, 279 | default_features, 280 | all_features, 281 | features, 282 | unstable_features, 283 | target, 284 | target_dir, 285 | target_args, 286 | manifest_path, 287 | package, 288 | workspace, 289 | offline, 290 | timings, 291 | no_std, 292 | bin, 293 | locked, 294 | frozen, 295 | verbose, 296 | bins, 297 | no_build, 298 | }) 299 | .context("Failed to cook recipe.")?; 300 | } 301 | Command::Prepare(Prepare { recipe_path, bin }) => { 302 | let recipe = 303 | Recipe::prepare(current_directory, bin).context("Failed to compute recipe")?; 304 | let serialized = 305 | serde_json::to_string(&recipe).context("Failed to serialize recipe.")?; 306 | fs::write(recipe_path, serialized).context("Failed to save recipe to 'recipe.json'")?; 307 | } 308 | } 309 | Ok(()) 310 | } 311 | 312 | fn main() -> Result<(), anyhow::Error> { 313 | env_logger::init(); 314 | _main() 315 | } 316 | -------------------------------------------------------------------------------- /src/recipe.rs: -------------------------------------------------------------------------------- 1 | use crate::Skeleton; 2 | use anyhow::Context; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashSet; 5 | use std::path::PathBuf; 6 | use std::process::Command; 7 | 8 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 9 | pub struct Recipe { 10 | pub skeleton: Skeleton, 11 | } 12 | 13 | pub struct TargetArgs { 14 | pub benches: bool, 15 | pub tests: bool, 16 | pub examples: bool, 17 | pub all_targets: bool, 18 | } 19 | 20 | pub enum CommandArg { 21 | Build, 22 | Check, 23 | Clippy, 24 | Zigbuild, 25 | NoBuild, 26 | } 27 | 28 | pub struct CookArgs { 29 | pub profile: OptimisationProfile, 30 | pub command: CommandArg, 31 | pub default_features: DefaultFeatures, 32 | pub all_features: AllFeatures, 33 | pub features: Option>, 34 | pub unstable_features: Option>, 35 | pub target: Option>, 36 | pub target_dir: Option, 37 | pub target_args: TargetArgs, 38 | pub manifest_path: Option, 39 | pub package: Option>, 40 | pub workspace: bool, 41 | pub offline: bool, 42 | pub locked: bool, 43 | pub frozen: bool, 44 | pub verbose: bool, 45 | pub timings: bool, 46 | pub no_std: bool, 47 | pub bin: Option>, 48 | pub bins: bool, 49 | pub no_build: bool, 50 | } 51 | 52 | impl Recipe { 53 | pub fn prepare(base_path: PathBuf, member: Option) -> Result { 54 | let skeleton = Skeleton::derive(base_path, member)?; 55 | Ok(Recipe { skeleton }) 56 | } 57 | 58 | pub fn cook(&self, args: CookArgs) -> Result<(), anyhow::Error> { 59 | let current_directory = std::env::current_dir()?; 60 | self.skeleton 61 | .build_minimum_project(¤t_directory, args.no_std)?; 62 | if args.no_build { 63 | return Ok(()); 64 | } 65 | build_dependencies(&args); 66 | self.skeleton 67 | .remove_compiled_dummies( 68 | current_directory, 69 | args.profile, 70 | args.target, 71 | args.target_dir, 72 | ) 73 | .context("Failed to clean up dummy compilation artifacts.")?; 74 | Ok(()) 75 | } 76 | } 77 | 78 | #[derive(Debug, Clone, Eq, PartialEq)] 79 | pub enum OptimisationProfile { 80 | Release, 81 | Debug, 82 | Other(String), 83 | } 84 | 85 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 86 | pub enum DefaultFeatures { 87 | Enabled, 88 | Disabled, 89 | } 90 | 91 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 92 | pub enum AllFeatures { 93 | Enabled, 94 | Disabled, 95 | } 96 | 97 | fn build_dependencies(args: &CookArgs) { 98 | let CookArgs { 99 | profile, 100 | command: command_arg, 101 | default_features, 102 | all_features, 103 | features, 104 | unstable_features, 105 | target, 106 | target_dir, 107 | target_args, 108 | manifest_path, 109 | package, 110 | workspace, 111 | offline, 112 | frozen, 113 | locked, 114 | verbose, 115 | timings, 116 | bin, 117 | no_std: _no_std, 118 | bins, 119 | no_build: _no_build, 120 | } = args; 121 | let cargo_path = std::env::var("CARGO").expect("The `CARGO` environment variable was not set. This is unexpected: it should always be provided by `cargo` when invoking a custom sub-command, allowing `cargo-chef` to correctly detect which toolchain should be used. Please file a bug."); 122 | let mut command = Command::new(cargo_path); 123 | let command_with_args = match command_arg { 124 | CommandArg::Build => command.arg("build"), 125 | CommandArg::Check => command.arg("check"), 126 | CommandArg::Clippy => command.arg("clippy"), 127 | CommandArg::Zigbuild => command.arg("zigbuild"), 128 | CommandArg::NoBuild => return, 129 | }; 130 | if profile == &OptimisationProfile::Release { 131 | command_with_args.arg("--release"); 132 | } else if let OptimisationProfile::Other(custom_profile) = profile { 133 | command_with_args.arg("--profile").arg(custom_profile); 134 | } 135 | if default_features == &DefaultFeatures::Disabled { 136 | command_with_args.arg("--no-default-features"); 137 | } 138 | if let Some(features) = features { 139 | let feature_flag = features.iter().cloned().collect::>().join(","); 140 | command_with_args.arg("--features").arg(feature_flag); 141 | } 142 | if all_features == &AllFeatures::Enabled { 143 | command_with_args.arg("--all-features"); 144 | } 145 | if let Some(unstable_features) = unstable_features { 146 | for unstable_feature in unstable_features.iter().cloned() { 147 | command_with_args.arg("-Z").arg(unstable_feature); 148 | } 149 | } 150 | if let Some(target) = target { 151 | for target in target { 152 | command_with_args.arg("--target").arg(target); 153 | } 154 | } 155 | if let Some(target_dir) = target_dir { 156 | command_with_args.arg("--target-dir").arg(target_dir); 157 | } 158 | if target_args.benches { 159 | command_with_args.arg("--benches"); 160 | } 161 | if target_args.tests { 162 | command_with_args.arg("--tests"); 163 | } 164 | if target_args.examples { 165 | command_with_args.arg("--examples"); 166 | } 167 | if target_args.all_targets { 168 | command_with_args.arg("--all-targets"); 169 | } 170 | if let Some(manifest_path) = manifest_path { 171 | command_with_args.arg("--manifest-path").arg(manifest_path); 172 | } 173 | if let Some(package) = package { 174 | for package in package { 175 | command_with_args.arg("--package").arg(package); 176 | } 177 | } 178 | if let Some(binary_target) = bin { 179 | for binary_target in binary_target { 180 | command_with_args.arg("--bin").arg(binary_target); 181 | } 182 | } 183 | if *workspace { 184 | command_with_args.arg("--workspace"); 185 | } 186 | if *offline { 187 | command_with_args.arg("--offline"); 188 | } 189 | if *frozen { 190 | command_with_args.arg("--frozen"); 191 | } 192 | if *locked { 193 | command_with_args.arg("--locked"); 194 | } 195 | if *verbose { 196 | command_with_args.arg("--verbose"); 197 | } 198 | if *timings { 199 | command_with_args.arg("--timings"); 200 | } 201 | if *bins { 202 | command_with_args.arg("--bins"); 203 | } 204 | 205 | execute_command(command_with_args); 206 | } 207 | 208 | fn execute_command(command: &mut Command) { 209 | let mut child = command 210 | .envs(std::env::vars()) 211 | .spawn() 212 | .expect("Failed to execute process"); 213 | 214 | let exit_status = child.wait().expect("Failed to run command"); 215 | 216 | if !exit_status.success() { 217 | match exit_status.code() { 218 | Some(code) => panic!("Exited with status code: {}", code), 219 | None => panic!("Process terminated by signal"), 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/skeleton/mod.rs: -------------------------------------------------------------------------------- 1 | mod read; 2 | mod target; 3 | mod version_masking; 4 | 5 | use crate::skeleton::target::{Target, TargetKind}; 6 | use crate::OptimisationProfile; 7 | use anyhow::Context; 8 | use cargo_manifest::Product; 9 | use cargo_metadata::Metadata; 10 | use fs_err as fs; 11 | use globwalk::GlobWalkerBuilder; 12 | use pathdiff::diff_paths; 13 | use serde::{Deserialize, Serialize}; 14 | use std::path::{Path, PathBuf}; 15 | 16 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 17 | pub struct Skeleton { 18 | pub manifests: Vec, 19 | pub config_file: Option, 20 | pub lock_file: Option, 21 | pub rust_toolchain_file: Option<(RustToolchainFile, String)>, 22 | } 23 | 24 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 25 | pub enum RustToolchainFile { 26 | Bare, 27 | Toml, 28 | } 29 | 30 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 31 | pub struct Manifest { 32 | /// Relative path with respect to the project root. 33 | pub relative_path: PathBuf, 34 | pub contents: String, 35 | pub targets: Vec, 36 | } 37 | 38 | pub(in crate::skeleton) struct ParsedManifest { 39 | relative_path: PathBuf, 40 | contents: toml::Value, 41 | targets: Vec, 42 | } 43 | 44 | impl Skeleton { 45 | /// Find all Cargo.toml files in `base_path` by traversing sub-directories recursively. 46 | pub fn derive>( 47 | base_path: P, 48 | member: Option, 49 | ) -> Result { 50 | let metadata = extract_cargo_metadata(base_path.as_ref())?; 51 | 52 | // Read relevant files from the filesystem 53 | let config_file = read::config(&base_path)?; 54 | let mut manifests = read::manifests(&base_path, &metadata)?; 55 | if let Some(member) = member { 56 | ignore_all_members_except(&mut manifests, &metadata, member); 57 | } 58 | 59 | let mut lock_file = read::lockfile(&base_path)?; 60 | let rust_toolchain_file = read::rust_toolchain(&base_path)?; 61 | 62 | version_masking::mask_local_crate_versions(&mut manifests, &mut lock_file); 63 | 64 | let lock_file = lock_file.map(|l| toml::to_string(&l)).transpose()?; 65 | 66 | let mut serialised_manifests = serialize_manifests(manifests)?; 67 | // We don't want an ordering issue (e.g. related to how files are read from the filesystem) 68 | // to make our skeleton generation logic non-reproducible - therefore we sort! 69 | serialised_manifests.sort_by_key(|m| m.relative_path.clone()); 70 | 71 | Ok(Skeleton { 72 | manifests: serialised_manifests, 73 | config_file, 74 | lock_file, 75 | rust_toolchain_file, 76 | }) 77 | } 78 | 79 | /// Given the manifests in the current skeleton, create the minimum set of files required to 80 | /// have a valid Rust project (i.e. write all manifests to disk and create dummy `lib.rs`, 81 | /// `main.rs` and `build.rs` files where needed). 82 | /// 83 | /// This function should be called on an empty canvas - i.e. an empty directory apart from 84 | /// the recipe file used to restore the skeleton. 85 | pub fn build_minimum_project( 86 | &self, 87 | base_path: &Path, 88 | no_std: bool, 89 | ) -> Result<(), anyhow::Error> { 90 | // Save lockfile to disk, if available 91 | if let Some(lock_file) = &self.lock_file { 92 | let lock_file_path = base_path.join("Cargo.lock"); 93 | fs::write(lock_file_path, lock_file.as_str())?; 94 | } 95 | 96 | // Save rust-toolchain or rust-toolchain.toml to disk, if available 97 | if let Some((file_kind, content)) = &self.rust_toolchain_file { 98 | let file_name = match file_kind { 99 | RustToolchainFile::Bare => "rust-toolchain", 100 | RustToolchainFile::Toml => "rust-toolchain.toml", 101 | }; 102 | let path = base_path.join(file_name); 103 | fs::write(path, content.as_str())?; 104 | } 105 | 106 | // save config file to disk, if available 107 | if let Some(config_file) = &self.config_file { 108 | let parent_dir = base_path.join(".cargo"); 109 | let config_file_path = parent_dir.join("config.toml"); 110 | fs::create_dir_all(parent_dir)?; 111 | fs::write(config_file_path, config_file.as_str())?; 112 | } 113 | 114 | const NO_STD_ENTRYPOINT: &str = "#![no_std] 115 | #![no_main] 116 | 117 | #[panic_handler] 118 | fn panic(_: &core::panic::PanicInfo) -> ! { 119 | loop {} 120 | } 121 | "; 122 | const NO_STD_HARNESS_ENTRYPOINT: &str = r#"#![no_std] 123 | #![no_main] 124 | #![feature(custom_test_frameworks)] 125 | #![test_runner(test_runner)] 126 | 127 | #[no_mangle] 128 | pub extern "C" fn _init() {} 129 | 130 | fn test_runner(_: &[&dyn Fn()]) {} 131 | 132 | #[panic_handler] 133 | fn panic(_: &core::panic::PanicInfo) -> ! { 134 | loop {} 135 | } 136 | "#; 137 | 138 | let get_test_like_entrypoint = |harness: bool| -> &str { 139 | match (no_std, harness) { 140 | (true, true) => NO_STD_HARNESS_ENTRYPOINT, 141 | (true, false) => NO_STD_ENTRYPOINT, 142 | (false, true) => "", 143 | (false, false) => "fn main() {}", 144 | } 145 | }; 146 | 147 | // Save all manifests to disks 148 | for manifest in &self.manifests { 149 | // Persist manifest 150 | let manifest_path = base_path.join(&manifest.relative_path); 151 | let parent_directory = if let Some(parent_directory) = manifest_path.parent() { 152 | fs::create_dir_all(parent_directory)?; 153 | parent_directory.to_path_buf() 154 | } else { 155 | base_path.to_path_buf() 156 | }; 157 | fs::write(&manifest_path, &manifest.contents)?; 158 | let parsed_manifest = 159 | cargo_manifest::Manifest::from_slice(manifest.contents.as_bytes())?; 160 | 161 | let is_harness = |products: &[Product], name: &str| -> bool { 162 | products 163 | .iter() 164 | .find(|product| product.name.as_deref() == Some(name)) 165 | .map(|p| p.harness) 166 | .unwrap_or(true) 167 | }; 168 | 169 | // Create dummy entrypoints for all targets 170 | for target in &manifest.targets { 171 | let content = match target.kind { 172 | TargetKind::BuildScript => "fn main() {}", 173 | TargetKind::Bin | TargetKind::Example => { 174 | if no_std { 175 | NO_STD_ENTRYPOINT 176 | } else { 177 | "fn main() {}" 178 | } 179 | } 180 | TargetKind::Lib { is_proc_macro } => { 181 | if no_std && !is_proc_macro { 182 | "#![no_std]" 183 | } else { 184 | "" 185 | } 186 | } 187 | TargetKind::Bench => { 188 | get_test_like_entrypoint(is_harness(&parsed_manifest.bench, &target.name)) 189 | } 190 | TargetKind::Test => { 191 | get_test_like_entrypoint(is_harness(&parsed_manifest.test, &target.name)) 192 | } 193 | }; 194 | let path = parent_directory.join(&target.path); 195 | if let Some(dir) = path.parent() { 196 | fs::create_dir_all(dir)?; 197 | } 198 | fs::write(&path, content)?; 199 | } 200 | } 201 | Ok(()) 202 | } 203 | 204 | /// Scan the target directory and remove all compilation artifacts for libraries and build 205 | /// scripts from the current workspace. 206 | /// Given the usage of dummy `lib.rs` and `build.rs` files, keeping them around leads to funny 207 | /// compilation errors. 208 | pub fn remove_compiled_dummies>( 209 | &self, 210 | base_path: P, 211 | profile: OptimisationProfile, 212 | target: Option>, 213 | target_dir: Option, 214 | ) -> Result<(), anyhow::Error> { 215 | let target_dir = match target_dir { 216 | None => base_path.as_ref().join("target"), 217 | Some(target_dir) => target_dir, 218 | }; 219 | 220 | // https://doc.rust-lang.org/cargo/guide/build-cache.html 221 | // > For historical reasons, the `dev` and `test` profiles are stored 222 | // > in the `debug` directory, and the `release` and `bench` profiles are 223 | // > stored in the `release` directory. User-defined profiles are 224 | // > stored in a directory with the same name as the profile. 225 | 226 | let profile_dir = match &profile { 227 | OptimisationProfile::Release => "release", 228 | OptimisationProfile::Debug => "debug", 229 | OptimisationProfile::Other(profile) if profile == "bench" => "release", 230 | OptimisationProfile::Other(profile) if profile == "dev" || profile == "test" => "debug", 231 | OptimisationProfile::Other(custom_profile) => custom_profile, 232 | }; 233 | 234 | let target_directories: Vec = target 235 | .map_or(vec![target_dir.clone()], |targets| { 236 | targets 237 | .iter() 238 | .map(|target| target_dir.join(target_str(target))) 239 | .collect() 240 | }) 241 | .iter() 242 | .map(|path| path.join(profile_dir)) 243 | .collect(); 244 | 245 | for manifest in &self.manifests { 246 | let parsed_manifest = 247 | cargo_manifest::Manifest::from_slice(manifest.contents.as_bytes())?; 248 | if let Some(package) = parsed_manifest.package.as_ref() { 249 | for target_directory in &target_directories { 250 | // Remove dummy libraries. 251 | if let Some(lib) = &parsed_manifest.lib { 252 | let library_name = 253 | lib.name.as_ref().unwrap_or(&package.name).replace('-', "_"); 254 | let walker = GlobWalkerBuilder::from_patterns( 255 | target_directory, 256 | &[ 257 | format!("/**/lib{}.*", library_name), 258 | format!("/**/lib{}-*", library_name), 259 | ], 260 | ) 261 | .build()?; 262 | for file in walker { 263 | let file = file?; 264 | if file.file_type().is_file() { 265 | fs::remove_file(file.path())?; 266 | } else if file.file_type().is_dir() { 267 | fs::remove_dir_all(file.path())?; 268 | } 269 | } 270 | } 271 | 272 | // Remove dummy build.rs script artifacts. 273 | if package.build.is_some() { 274 | let walker = GlobWalkerBuilder::new( 275 | target_directory, 276 | format!("/build/{}-*/build[-_]script[-_]build*", package.name), 277 | ) 278 | .build()?; 279 | for file in walker { 280 | let file = file?; 281 | fs::remove_file(file.path())?; 282 | } 283 | } 284 | } 285 | } 286 | } 287 | 288 | Ok(()) 289 | } 290 | } 291 | 292 | /// If a custom target spec file is used, 293 | /// (Part of the unstable cargo feature 'build-std'; c.f. https://doc.rust-lang.org/rustc/targets/custom.html ) 294 | /// the `--target` flag refers to a `.json` file in the current directory. 295 | /// In this case, the actual name of the target is the value of `--target` without the `.json` suffix. 296 | fn target_str(target: &str) -> &str { 297 | target.trim_end_matches(".json") 298 | } 299 | 300 | fn serialize_manifests(manifests: Vec) -> Result, anyhow::Error> { 301 | let mut serialised_manifests = vec![]; 302 | for manifest in manifests { 303 | // The serialised contents might be different from the original manifest! 304 | let contents = toml::to_string(&manifest.contents)?; 305 | serialised_manifests.push(Manifest { 306 | relative_path: manifest.relative_path, 307 | contents, 308 | targets: manifest.targets, 309 | }); 310 | } 311 | Ok(serialised_manifests) 312 | } 313 | 314 | fn extract_cargo_metadata(path: &Path) -> Result { 315 | let mut cmd = cargo_metadata::MetadataCommand::new(); 316 | cmd.current_dir(path); 317 | cmd.no_deps(); 318 | 319 | cmd.exec().context("Cannot extract Cargo metadata") 320 | } 321 | 322 | /// If the top-level `Cargo.toml` has a `members` field, replace it with 323 | /// a list consisting of just the path to the package. 324 | /// 325 | /// Also deletes the `default-members` field because it does not play nicely 326 | /// with a modified `members` field and has no effect on cooking the final recipe. 327 | fn ignore_all_members_except( 328 | manifests: &mut [ParsedManifest], 329 | metadata: &Metadata, 330 | member: String, 331 | ) { 332 | let workspace_toml = manifests 333 | .iter_mut() 334 | .find(|manifest| manifest.relative_path == std::path::PathBuf::from("Cargo.toml")); 335 | 336 | if let Some(workspace) = workspace_toml.and_then(|toml| toml.contents.get_mut("workspace")) { 337 | if let Some(members) = workspace.get_mut("members") { 338 | let workspace_root = &metadata.workspace_root; 339 | let workspace_packages = metadata.workspace_packages(); 340 | 341 | if let Some(pkg) = workspace_packages 342 | .into_iter() 343 | .find(|pkg| pkg.name == member) 344 | { 345 | // Make this a relative path to the workspace, and remove the `Cargo.toml` child. 346 | let member_cargo_path = diff_paths(pkg.manifest_path.as_os_str(), workspace_root); 347 | let member_workspace_path = member_cargo_path 348 | .as_ref() 349 | .and_then(|path| path.parent()) 350 | .and_then(|dir| dir.to_str()); 351 | 352 | if let Some(member_path) = member_workspace_path { 353 | *members = 354 | toml::Value::Array(vec![toml::Value::String(member_path.to_string())]); 355 | } 356 | } 357 | } 358 | if let Some(workspace) = workspace.as_table_mut() { 359 | workspace.remove("default-members"); 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/skeleton/read.rs: -------------------------------------------------------------------------------- 1 | //! Logic to read all the files required to build a caching layer for a project. 2 | use super::ParsedManifest; 3 | use crate::skeleton::target::{Target, TargetKind}; 4 | use crate::RustToolchainFile; 5 | use cargo_metadata::{Metadata, Package}; 6 | use std::collections::{BTreeMap, BTreeSet}; 7 | use std::fs; 8 | use std::path::{Path, PathBuf}; 9 | use std::str::FromStr; 10 | 11 | pub(super) fn config>(base_path: &P) -> Result, anyhow::Error> { 12 | // Given that we run primarily in Docker, assume to find config or config.toml at root level. 13 | // We give priority to config over config.toml since this is cargo's default behavior. 14 | 15 | let file_contents = |file: &str| { 16 | fs::read_to_string( 17 | base_path 18 | .as_ref() 19 | .join(".cargo") 20 | .join(file) 21 | .into_os_string(), 22 | ) 23 | }; 24 | 25 | let config = file_contents("config").or_else(|_| file_contents("config.toml")); 26 | 27 | match config { 28 | Ok(config) => Ok(Some(config)), 29 | Err(e) => { 30 | if std::io::ErrorKind::NotFound != e.kind() { 31 | return Err( 32 | anyhow::Error::from(e).context("Failed to read .cargo/config.toml file.") 33 | ); 34 | } 35 | Ok(None) 36 | } 37 | } 38 | } 39 | 40 | pub(super) fn manifests>( 41 | base_path: &P, 42 | metadata: &Metadata, 43 | ) -> Result, anyhow::Error> { 44 | let mut packages: BTreeMap> = metadata 45 | .workspace_packages() 46 | .iter() 47 | .copied() 48 | .chain(metadata.root_package()) 49 | .map(|p| { 50 | ( 51 | p.manifest_path.clone().into_std_path_buf(), 52 | gather_targets(p), 53 | ) 54 | }) 55 | .collect(); 56 | 57 | if metadata.root_package().is_none() { 58 | // At the root, there might be a Cargo.toml manifest with a [workspace] section. 59 | // However, if this root manifest doesn't contain [package], it is not considered a package 60 | // by cargo metadata. Therefore, we have to add it manually. 61 | // Workspaces currently cannot be nested, so this should only happen at the root. 62 | packages.insert(base_path.as_ref().join("Cargo.toml"), Default::default()); 63 | } 64 | 65 | let mut manifests = vec![]; 66 | for (absolute_path, targets) in packages { 67 | let contents = fs::read_to_string(&absolute_path)?; 68 | 69 | let mut parsed = cargo_manifest::Manifest::from_str(&contents)?; 70 | // Required to detect bin/libs when the related section is omitted from the manifest 71 | parsed.complete_from_path(&absolute_path)?; 72 | 73 | let mut intermediate = toml::Value::try_from(parsed)?; 74 | 75 | // Specifically, toml gives no guarantees to the ordering of the auto binaries 76 | // in its results. We will manually sort these to ensure that the output 77 | // manifest will match. 78 | let bins = intermediate 79 | .get_mut("bin") 80 | .and_then(|bins| bins.as_array_mut()); 81 | if let Some(bins) = bins { 82 | bins.sort_by(|bin_a, bin_b| { 83 | let bin_a_path = bin_a 84 | .as_table() 85 | .and_then(|table| table.get("path").or_else(|| table.get("name"))) 86 | .and_then(|path| path.as_str()) 87 | .unwrap(); 88 | let bin_b_path = bin_b 89 | .as_table() 90 | .and_then(|table| table.get("path").or_else(|| table.get("name"))) 91 | .and_then(|path| path.as_str()) 92 | .unwrap(); 93 | bin_a_path.cmp(bin_b_path) 94 | }); 95 | } 96 | 97 | let relative_path = pathdiff::diff_paths(&absolute_path, base_path).ok_or_else(|| { 98 | anyhow::anyhow!( 99 | "Failed to compute relative path of manifest {:?}", 100 | &absolute_path 101 | ) 102 | })?; 103 | 104 | manifests.push(ParsedManifest { 105 | relative_path, 106 | contents: intermediate, 107 | targets: targets.into_iter().collect(), 108 | }); 109 | } 110 | 111 | Ok(manifests) 112 | } 113 | 114 | fn gather_targets(package: &Package) -> BTreeSet { 115 | let manifest_path = package.manifest_path.clone().into_std_path_buf(); 116 | let root_dir = manifest_path.parent().unwrap(); 117 | package 118 | .targets 119 | .iter() 120 | .map(|target| { 121 | let relative_path = pathdiff::diff_paths(&target.src_path, root_dir).unwrap(); 122 | let kind = if target.is_bench() { 123 | TargetKind::Bench 124 | } else if target.is_example() { 125 | TargetKind::Example 126 | } else if target.is_test() { 127 | TargetKind::Test 128 | } else if target.is_bin() { 129 | TargetKind::Bin 130 | } else if target.is_custom_build() { 131 | TargetKind::BuildScript 132 | } else { 133 | // If a library has custom crate type (e.g. "cdylib"), it's kind will be "cdylib" 134 | // instead of just "lib". Therefore, we assume that this target is a library. 135 | TargetKind::Lib { 136 | is_proc_macro: target 137 | .crate_types 138 | .iter() 139 | .any(|t| t.as_str() == "proc-macro"), 140 | } 141 | }; 142 | 143 | Target { 144 | path: relative_path, 145 | kind, 146 | name: target.name.clone(), 147 | } 148 | }) 149 | .collect() 150 | } 151 | 152 | pub(super) fn lockfile>( 153 | base_path: &P, 154 | ) -> Result, anyhow::Error> { 155 | match fs::read_to_string(base_path.as_ref().join("Cargo.lock")) { 156 | Ok(lock) => { 157 | let lock: toml::Value = toml::from_str(&lock)?; 158 | Ok(Some(lock)) 159 | } 160 | Err(e) => { 161 | if std::io::ErrorKind::NotFound != e.kind() { 162 | return Err(anyhow::Error::from(e).context("Failed to read Cargo.lock file.")); 163 | } 164 | Ok(None) 165 | } 166 | } 167 | } 168 | 169 | pub(super) fn rust_toolchain>( 170 | base_path: &P, 171 | ) -> Result, anyhow::Error> { 172 | // `rust-toolchain` takes precedence over `rust-toolchain.toml` 173 | if let Some(file) = read_rust_toolchain(&base_path.as_ref().join("rust-toolchain"))? { 174 | return Ok(Some((RustToolchainFile::Bare, file))); 175 | } 176 | 177 | if let Some(file) = read_rust_toolchain(&base_path.as_ref().join("rust-toolchain.toml"))? { 178 | return Ok(Some((RustToolchainFile::Toml, file))); 179 | } 180 | 181 | Ok(None) 182 | } 183 | 184 | fn read_rust_toolchain(path: &Path) -> Result, anyhow::Error> { 185 | match fs::read_to_string(path) { 186 | Ok(file) => Ok(Some(file)), 187 | Err(e) => { 188 | if std::io::ErrorKind::NotFound != e.kind() { 189 | Err(anyhow::Error::from(e).context("Failed to read rust toolchain file.")) 190 | } else { 191 | Ok(None) 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/skeleton/target.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] 4 | pub enum TargetKind { 5 | Lib { is_proc_macro: bool }, 6 | Bin, 7 | Test, 8 | Bench, 9 | Example, 10 | BuildScript, 11 | } 12 | 13 | #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] 14 | pub struct Target { 15 | pub(crate) path: PathBuf, 16 | pub(crate) kind: TargetKind, 17 | pub(crate) name: String, 18 | } 19 | -------------------------------------------------------------------------------- /src/skeleton/version_masking.rs: -------------------------------------------------------------------------------- 1 | use super::ParsedManifest; 2 | 3 | /// All local dependencies are emptied out when running `prepare`. 4 | /// We do not want the recipe file to change if the only difference with 5 | /// the previous docker build attempt is the version of a local crate 6 | /// encoded in `Cargo.lock` (while the remote dependency tree 7 | /// is unchanged) or in the corresponding `Cargo.toml` manifest. 8 | /// We replace versions of local crates in `Cargo.lock` and in all `Cargo.toml`s, including 9 | /// when specified as dependency of another crate in the workspace. 10 | pub(super) fn mask_local_crate_versions( 11 | manifests: &mut [ParsedManifest], 12 | lock_file: &mut Option, 13 | ) { 14 | let local_package_names = parse_local_crate_names(manifests); 15 | mask_local_versions_in_manifests(manifests, &local_package_names); 16 | if let Some(l) = lock_file { 17 | mask_local_versions_in_lockfile(l, &local_package_names); 18 | } 19 | } 20 | 21 | /// Dummy version used for all local crates. 22 | const CONST_VERSION: &str = "0.0.1"; 23 | 24 | fn mask_local_versions_in_lockfile( 25 | lock_file: &mut toml::Value, 26 | local_package_names: &[toml::Value], 27 | ) { 28 | if let Some(packages) = lock_file 29 | .get_mut("package") 30 | .and_then(|packages| packages.as_array_mut()) 31 | { 32 | packages 33 | .iter_mut() 34 | // Find all local crates 35 | .filter(|package| { 36 | package 37 | .get("name") 38 | .map(|name| local_package_names.contains(name)) 39 | .unwrap_or_default() 40 | && package.get("source").is_none() 41 | }) 42 | // Mask the version 43 | .for_each(|package| { 44 | if let Some(version) = package.get_mut("version") { 45 | *version = toml::Value::String(CONST_VERSION.to_string()) 46 | } 47 | }); 48 | } 49 | } 50 | 51 | fn mask_local_versions_in_manifests( 52 | manifests: &mut [ParsedManifest], 53 | local_package_names: &[toml::Value], 54 | ) { 55 | for manifest in manifests.iter_mut() { 56 | if let Some(package) = manifest.contents.get_mut("package") { 57 | if let Some(version) = package.get_mut("version") { 58 | if version.as_str().is_some() { 59 | *version = toml::Value::String(CONST_VERSION.to_string()); 60 | } 61 | } 62 | } 63 | mask_local_dependency_versions(local_package_names, manifest); 64 | } 65 | } 66 | 67 | fn mask_local_dependency_versions( 68 | local_package_names: &[toml::Value], 69 | manifest: &mut ParsedManifest, 70 | ) { 71 | fn _mask(local_package_names: &[toml::Value], toml_value: &mut toml::Value) { 72 | for dependency_key in ["dependencies", "dev-dependencies", "build-dependencies"] { 73 | if let Some(dependencies) = toml_value.get_mut(dependency_key) { 74 | if let Some(dependencies) = dependencies.as_table_mut() { 75 | for (key, dependency) in dependencies { 76 | if dependency.get("path").is_none() { 77 | // This dependency is not local 78 | continue; 79 | } 80 | 81 | let mut must_mark_version = false; 82 | 83 | if let Some(package_name) = dependency.get("package") { 84 | // We are dealing with a renamed package, so we check the name of the 85 | // "source" package. 86 | if local_package_names.contains(package_name) { 87 | must_mark_version = true; 88 | } 89 | } else { 90 | // The package has not been renamed, so we check the name of the 91 | // key in the dependencies table. 92 | if local_package_names.contains(&toml::Value::String(key.to_string())) { 93 | must_mark_version = true; 94 | } 95 | } 96 | 97 | if must_mark_version { 98 | if let Some(version) = dependency.get_mut("version") { 99 | *version = toml::Value::String(CONST_VERSION.to_string()); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | // There are three ways to specify dependencies: 109 | // - top-level 110 | // ```toml 111 | // [dependencies] 112 | // # [...] 113 | // ``` 114 | // - target-specific (e.g. Windows-only) 115 | // ```toml 116 | // [target.'cfg(windows)'.dependencies] 117 | // winhttp = "0.4.0" 118 | // ``` 119 | // The inner structure for target-specific dependencies mirrors the structure expected 120 | // for top-level dependencies. 121 | // Check out cargo's documentation (https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html) 122 | // for more details. 123 | _mask(local_package_names, &mut manifest.contents); 124 | if let Some(targets) = manifest.contents.get_mut("target") { 125 | if let Some(target_table) = targets.as_table_mut() { 126 | for (_, target_config) in target_table.iter_mut() { 127 | _mask(local_package_names, target_config) 128 | } 129 | } 130 | } 131 | 132 | // The third way to specify dependencies was introduced in rust 1.64: workspace inheritance. 133 | // ```toml 134 | // [workspace.dependencies] 135 | // anyhow = "1.0.66" 136 | // project_a = { path = "project_a", version = "0.2.0" } 137 | // ``` 138 | // Check out cargo's documentation (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-workspacedependencies-table) 139 | // for more details. 140 | if let Some(workspace) = manifest.contents.get_mut("workspace") { 141 | // Mask the workspace package version 142 | if let Some(package) = workspace.get_mut("package") { 143 | if let Some(version) = package.get_mut("version") { 144 | *version = toml::Value::String(CONST_VERSION.to_string()); 145 | } 146 | } 147 | // Mask the local crates in the workspace dependencies 148 | _mask(local_package_names, workspace); 149 | } 150 | } 151 | 152 | fn parse_local_crate_names(manifests: &[ParsedManifest]) -> Vec { 153 | let mut local_package_names = vec![]; 154 | for manifest in manifests.iter() { 155 | if let Some(package) = manifest.contents.get("package") { 156 | if let Some(name) = package.get("name") { 157 | local_package_names.push(name.to_owned()); 158 | } 159 | } 160 | } 161 | local_package_names 162 | } 163 | -------------------------------------------------------------------------------- /tests/recipe.rs: -------------------------------------------------------------------------------- 1 | use assert_fs::prelude::{FileTouch, FileWriteStr, PathChild, PathCreateDir}; 2 | use assert_fs::TempDir; 3 | use chef::Recipe; 4 | 5 | fn quick_recipe(content: &str) -> Recipe { 6 | let recipe_directory = TempDir::new().unwrap(); 7 | let manifest = recipe_directory.child("Cargo.toml"); 8 | manifest.write_str(content).unwrap(); 9 | let bin_dir = recipe_directory.child("src").child("bin"); 10 | let test_dir = recipe_directory.child("tests"); 11 | bin_dir.create_dir_all().unwrap(); 12 | test_dir.create_dir_all().unwrap(); 13 | for filename in &["f.rs", "e.rs", "d.rs", "c.rs", "b.rs", "a.rs"] { 14 | bin_dir.child(filename).touch().unwrap(); 15 | test_dir.child(filename).touch().unwrap(); 16 | } 17 | Recipe::prepare(recipe_directory.path().canonicalize().unwrap(), None).unwrap() 18 | } 19 | 20 | #[test] 21 | fn test_recipe_is_deterministic() { 22 | let content = r#" 23 | [package] 24 | name = "test-dummy" 25 | version = "0.1.0" 26 | edition = "2018" 27 | 28 | [[bin]] 29 | name = "bin2" 30 | path = "some-path.rs" 31 | 32 | [[bin]] 33 | name = "bin1" 34 | path = "some-other-path.rs" 35 | 36 | [[test]] 37 | name = "test2" 38 | path = "some-other-path.rs" 39 | 40 | [[test]] 41 | name = "test1" 42 | path = "some-path.rs" 43 | "#; 44 | let recipe = quick_recipe(content); 45 | let recipe_json = serde_json::to_string(&recipe).unwrap(); 46 | // construct a recipe a bunch more times and assert that each time 47 | // it is equal to the first (both the object and the json serialization) 48 | for _ in 0..5 { 49 | let recipe2 = quick_recipe(content); 50 | let recipe2_json = serde_json::to_string(&recipe).unwrap(); 51 | assert_eq!( 52 | recipe, recipe2, 53 | "recipes of equal directories are not equal" 54 | ); 55 | assert_eq!( 56 | recipe_json, recipe2_json, 57 | "recipe jsons of equal directories are not equal" 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/skeletons.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use assert_fs::prelude::*; 5 | use assert_fs::TempDir; 6 | use chef::Skeleton; 7 | use expect_test::{expect, Expect}; 8 | use predicates::prelude::*; 9 | 10 | #[test] 11 | pub fn no_workspace() { 12 | // Arrange 13 | let project = CargoWorkspace::new() 14 | .manifest( 15 | ".", 16 | r#" 17 | [package] 18 | name = "test-dummy" 19 | version = "0.1.0" 20 | edition = "2018" 21 | 22 | [[bin]] 23 | name = "test-dummy" 24 | path = "src/main.rs" 25 | 26 | [dependencies] 27 | "#, 28 | ) 29 | .touch("src/main.rs") 30 | .touch("Cargo.lock") 31 | .build(); 32 | 33 | // Act 34 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 35 | let cook_directory = TempDir::new().unwrap(); 36 | skeleton 37 | .build_minimum_project(cook_directory.path(), false) 38 | .unwrap(); 39 | 40 | // Assert 41 | assert_eq!(1, skeleton.manifests.len()); 42 | let manifest = &skeleton.manifests[0]; 43 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 44 | cook_directory 45 | .child("src") 46 | .child("main.rs") 47 | .assert("fn main() {}"); 48 | cook_directory 49 | .child("Cargo.lock") 50 | .assert(predicate::path::exists()); 51 | 52 | // Act (no_std) 53 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 54 | let cook_directory = TempDir::new().unwrap(); 55 | skeleton 56 | .build_minimum_project(cook_directory.path(), true) 57 | .unwrap(); 58 | 59 | // Assert (no_std) 60 | assert_eq!(1, skeleton.manifests.len()); 61 | let manifest = &skeleton.manifests[0]; 62 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 63 | cook_directory.child("src").child("main.rs").assert( 64 | r#"#![no_std] 65 | #![no_main] 66 | 67 | #[panic_handler] 68 | fn panic(_: &core::panic::PanicInfo) -> ! { 69 | loop {} 70 | } 71 | "#, 72 | ); 73 | cook_directory 74 | .child("Cargo.lock") 75 | .assert(predicate::path::exists()); 76 | } 77 | 78 | #[test] 79 | pub fn workspace() { 80 | // Arrange 81 | let project = CargoWorkspace::new() 82 | .manifest( 83 | ".", 84 | r#" 85 | [workspace] 86 | members = [ 87 | "src/project_a", 88 | "src/project_b", 89 | ] 90 | "#, 91 | ) 92 | .bin_package( 93 | "src/project_a", 94 | r#" 95 | [package] 96 | name = "project_a" 97 | version = "0.1.0" 98 | edition = "2018" 99 | 100 | [[bin]] 101 | name = "test-dummy" 102 | path = "src/main.rs" 103 | 104 | [dependencies] 105 | uuid = { version = "=0.8.0", features = ["v4"] } 106 | "#, 107 | ) 108 | .lib_package( 109 | "src/project_b", 110 | r#" 111 | [package] 112 | name = "project_b" 113 | version = "0.1.0" 114 | edition = "2018" 115 | 116 | [lib] 117 | crate-type = ["cdylib"] 118 | 119 | [dependencies] 120 | uuid = { version = "=0.8.0", features = ["v4"] } 121 | "#, 122 | ) 123 | .build(); 124 | 125 | // Act 126 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 127 | let cook_directory = TempDir::new().unwrap(); 128 | skeleton 129 | .build_minimum_project(cook_directory.path(), false) 130 | .unwrap(); 131 | 132 | // Assert 133 | assert_eq!(3, skeleton.manifests.len()); 134 | cook_directory 135 | .child("src") 136 | .child("project_a") 137 | .child("src") 138 | .child("main.rs") 139 | .assert("fn main() {}"); 140 | cook_directory 141 | .child("src") 142 | .child("project_b") 143 | .child("src") 144 | .child("lib.rs") 145 | .assert(""); 146 | 147 | // Act (no_std) 148 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 149 | let cook_directory = TempDir::new().unwrap(); 150 | skeleton 151 | .build_minimum_project(cook_directory.path(), true) 152 | .unwrap(); 153 | 154 | // Assert (no_std) 155 | assert_eq!(3, skeleton.manifests.len()); 156 | cook_directory 157 | .child("src") 158 | .child("project_a") 159 | .child("src") 160 | .child("main.rs") 161 | .assert( 162 | r#"#![no_std] 163 | #![no_main] 164 | 165 | #[panic_handler] 166 | fn panic(_: &core::panic::PanicInfo) -> ! { 167 | loop {} 168 | } 169 | "#, 170 | ); 171 | cook_directory 172 | .child("src") 173 | .child("project_b") 174 | .child("src") 175 | .child("lib.rs") 176 | .assert("#![no_std]"); 177 | } 178 | 179 | #[test] 180 | pub fn benches() { 181 | // Arrange 182 | let project = CargoWorkspace::new() 183 | .lib_package( 184 | ".", 185 | r#" 186 | [package] 187 | name = "test-dummy" 188 | version = "0.1.0" 189 | edition = "2018" 190 | 191 | [[bench]] 192 | name = "basics" 193 | harness = false 194 | 195 | [dependencies] 196 | "#, 197 | ) 198 | .touch("benches/basics.rs") 199 | .build(); 200 | 201 | // Act 202 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 203 | let cook_directory = TempDir::new().unwrap(); 204 | skeleton 205 | .build_minimum_project(cook_directory.path(), false) 206 | .unwrap(); 207 | 208 | // Assert 209 | assert_eq!(1, skeleton.manifests.len()); 210 | let manifest = &skeleton.manifests[0]; 211 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 212 | cook_directory 213 | .child("benches") 214 | .child("basics.rs") 215 | .assert("fn main() {}"); 216 | 217 | // no_std benches are not a thing yet 218 | } 219 | 220 | #[test] 221 | pub fn tests() { 222 | // Arrange 223 | let project = CargoWorkspace::new() 224 | .lib_package( 225 | ".", 226 | r#" 227 | [package] 228 | name = "test-dummy" 229 | version = "0.1.0" 230 | edition = "2018" 231 | 232 | [[test]] 233 | name = "foo" 234 | "#, 235 | ) 236 | .touch("tests/foo.rs") 237 | .build(); 238 | 239 | // Act 240 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 241 | let cook_directory = TempDir::new().unwrap(); 242 | skeleton 243 | .build_minimum_project(cook_directory.path(), false) 244 | .unwrap(); 245 | 246 | // Assert 247 | assert_eq!(1, skeleton.manifests.len()); 248 | let manifest = &skeleton.manifests[0]; 249 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 250 | cook_directory.child("tests").child("foo.rs").assert(""); 251 | 252 | // Act (no_std) 253 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 254 | let cook_directory = TempDir::new().unwrap(); 255 | skeleton 256 | .build_minimum_project(cook_directory.path(), true) 257 | .unwrap(); 258 | 259 | // Assert (no_std) 260 | assert_eq!(1, skeleton.manifests.len()); 261 | let manifest = &skeleton.manifests[0]; 262 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 263 | cook_directory.child("tests").child("foo.rs").assert( 264 | r#"#![no_std] 265 | #![no_main] 266 | #![feature(custom_test_frameworks)] 267 | #![test_runner(test_runner)] 268 | 269 | #[no_mangle] 270 | pub extern "C" fn _init() {} 271 | 272 | fn test_runner(_: &[&dyn Fn()]) {} 273 | 274 | #[panic_handler] 275 | fn panic(_: &core::panic::PanicInfo) -> ! { 276 | loop {} 277 | } 278 | "#, 279 | ); 280 | } 281 | 282 | #[test] 283 | pub fn tests_no_harness() { 284 | // Arrange 285 | let project = CargoWorkspace::new() 286 | .lib_package( 287 | ".", 288 | r#" 289 | [package] 290 | name = "test-dummy" 291 | version = "0.1.0" 292 | edition = "2018" 293 | 294 | [[test]] 295 | name = "foo" 296 | harness = false 297 | "#, 298 | ) 299 | .touch("tests/foo.rs") 300 | .build(); 301 | 302 | // Act 303 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 304 | let cook_directory = TempDir::new().unwrap(); 305 | skeleton 306 | .build_minimum_project(cook_directory.path(), false) 307 | .unwrap(); 308 | 309 | cook_directory 310 | .child("tests") 311 | .child("foo.rs") 312 | .assert("fn main() {}"); 313 | } 314 | 315 | #[test] 316 | pub fn examples() { 317 | // Arrange 318 | let project = CargoWorkspace::new() 319 | .lib_package( 320 | ".", 321 | r#" 322 | [package] 323 | name = "test-dummy" 324 | version = "0.1.0" 325 | edition = "2018" 326 | 327 | [[example]] 328 | name = "foo" 329 | "#, 330 | ) 331 | .touch("examples/foo.rs") 332 | .build(); 333 | 334 | // Act 335 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 336 | let cook_directory = TempDir::new().unwrap(); 337 | skeleton 338 | .build_minimum_project(cook_directory.path(), false) 339 | .unwrap(); 340 | 341 | // Assert 342 | assert_eq!(1, skeleton.manifests.len()); 343 | let manifest = &skeleton.manifests[0]; 344 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 345 | cook_directory 346 | .child("examples") 347 | .child("foo.rs") 348 | .assert("fn main() {}"); 349 | 350 | // Act (no_std) 351 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 352 | let cook_directory = TempDir::new().unwrap(); 353 | skeleton 354 | .build_minimum_project(cook_directory.path(), true) 355 | .unwrap(); 356 | 357 | // Assert (no_std) 358 | assert_eq!(1, skeleton.manifests.len()); 359 | let manifest = &skeleton.manifests[0]; 360 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 361 | cook_directory.child("examples").child("foo.rs").assert( 362 | r#"#![no_std] 363 | #![no_main] 364 | 365 | #[panic_handler] 366 | fn panic(_: &core::panic::PanicInfo) -> ! { 367 | loop {} 368 | } 369 | "#, 370 | ); 371 | } 372 | 373 | #[test] 374 | pub fn test_auto_bin_ordering() { 375 | // Arrange 376 | let project = CargoWorkspace::new() 377 | .manifest( 378 | ".", 379 | r#" 380 | [package] 381 | name = "test-dummy" 382 | version = "0.1.0" 383 | edition = "2018" 384 | "#, 385 | ) 386 | .touch_multiple(&[ 387 | "src/bin/a.rs", 388 | "src/bin/b.rs", 389 | "src/bin/c.rs", 390 | "src/bin/d.rs", 391 | "src/bin/e.rs", 392 | "src/bin/f.rs", 393 | ]) 394 | .build(); 395 | 396 | // Act 397 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 398 | 399 | // What we're testing is that auto-directories come back in the same order. 400 | // Since it's possible that the directories just happen to come back in the 401 | // same order randomly, we'll run this a few times to increase the 402 | // likelihood of triggering the problem if it exists. 403 | for _ in 0..5 { 404 | let skeleton2 = Skeleton::derive(project.path(), None).unwrap(); 405 | assert_eq!( 406 | skeleton, skeleton2, 407 | "Skeletons of equal directories are not equal. Check [[bin]] ordering in manifest?" 408 | ); 409 | } 410 | } 411 | 412 | #[test] 413 | pub fn config_toml() { 414 | // Arrange 415 | let project = CargoWorkspace::new() 416 | .bin_package( 417 | ".", 418 | r#" 419 | [package] 420 | name = "test-dummy" 421 | version = "0.1.0" 422 | edition = "2018" 423 | 424 | [dependencies] 425 | "#, 426 | ) 427 | .touch(".cargo/config.toml") 428 | .build(); 429 | 430 | // Act 431 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 432 | let cook_directory = TempDir::new().unwrap(); 433 | skeleton 434 | .build_minimum_project(cook_directory.path(), false) 435 | .unwrap(); 436 | 437 | // Assert 438 | assert_eq!(1, skeleton.manifests.len()); 439 | let manifest = &skeleton.manifests[0]; 440 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 441 | cook_directory 442 | .child("src") 443 | .child("main.rs") 444 | .assert("fn main() {}"); 445 | cook_directory 446 | .child(".cargo") 447 | .child("config.toml") 448 | .assert(predicate::path::exists()); 449 | } 450 | 451 | #[test] 452 | pub fn version() { 453 | // Arrange 454 | let project = CargoWorkspace::new() 455 | .bin_package( 456 | ".", 457 | r#" 458 | [package] 459 | name = "test-dummy" 460 | version = "1.2.3" 461 | edition = "2018" 462 | 463 | [dependencies] 464 | "#, 465 | ) 466 | .build(); 467 | 468 | // Act 469 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 470 | let cook_directory = TempDir::new().unwrap(); 471 | skeleton 472 | .build_minimum_project(cook_directory.path(), false) 473 | .unwrap(); 474 | 475 | // Assert 476 | assert_eq!(1, skeleton.manifests.len()); 477 | let manifest = skeleton.manifests[0].clone(); 478 | assert!(manifest.contents.contains(r#"version = "0.0.1""#)); 479 | assert!(!manifest.contents.contains(r#"version = "1.2.3""#)); 480 | } 481 | 482 | #[test] 483 | pub fn version_lock() { 484 | // Arrange 485 | let project = CargoWorkspace::new() 486 | .bin_package( 487 | ".", 488 | r#" 489 | [package] 490 | name = "test-dummy" 491 | version = "1.2.3" 492 | edition = "2018" 493 | 494 | [dependencies] 495 | "#, 496 | ) 497 | .file( 498 | "Cargo.lock", 499 | r#" 500 | # This file is automatically @generated by Cargo. 501 | # It is not intended for manual editing. 502 | version = 3 503 | 504 | [[package]] 505 | name = "test-dummy" 506 | version = "1.2.3" 507 | "#, 508 | ) 509 | .build(); 510 | 511 | // Act 512 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 513 | let cook_directory = TempDir::new().unwrap(); 514 | skeleton 515 | .build_minimum_project(cook_directory.path(), false) 516 | .unwrap(); 517 | 518 | // Assert 519 | let lock_file = skeleton.lock_file.expect("there should be a lock_file"); 520 | assert!(!lock_file.contains( 521 | r#" 522 | [[package]] 523 | name = "test-dummy" 524 | version = "1.2.3" 525 | "# 526 | )); 527 | assert!(lock_file.contains( 528 | r#" 529 | [[package]] 530 | name = "test-dummy" 531 | version = "0.0.1" 532 | "# 533 | )); 534 | } 535 | 536 | #[test] 537 | pub fn workspace_version_lock() { 538 | // Arrange 539 | // project-a is named with a dash to test that such unnormalized name can be handled. 540 | let project = CargoWorkspace::new() 541 | .manifest( 542 | ".", 543 | r#" 544 | [workspace] 545 | members = [ 546 | "src/project-a", 547 | "src/project_b", 548 | ] 549 | "#, 550 | ) 551 | .bin_package( 552 | "src/project-a", 553 | r#" 554 | [package] 555 | name = "project-a" 556 | version = "1.2.3" 557 | edition = "2018" 558 | 559 | [[bin]] 560 | name = "test-dummy" 561 | path = "src/main.rs" 562 | 563 | [dependencies] 564 | either = { version = "=1.8.1" } 565 | "#, 566 | ) 567 | .lib_package( 568 | "src/project_b", 569 | r#" 570 | [package] 571 | name = "project_b" 572 | version = "4.5.6" 573 | edition = "2018" 574 | 575 | [lib] 576 | crate-type = ["cdylib"] 577 | 578 | [dependencies] 579 | either = { version = "=1.8.1" } 580 | project-a = { version = "1.2.3", path = "../project-a" } 581 | "#, 582 | ) 583 | .file( 584 | "Cargo.lock", 585 | r#" 586 | # This file is automatically @generated by Cargo. 587 | # It is not intended for manual editing. 588 | version = 3 589 | 590 | [[package]] 591 | name = "either" 592 | version = "1.8.1" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 595 | 596 | [[package]] 597 | name = "project-a" 598 | version = "1.2.3" 599 | dependencies = [ 600 | "either", 601 | ] 602 | 603 | [[package]] 604 | name = "project_b" 605 | version = "4.5.6" 606 | dependencies = [ 607 | "either", 608 | "project_a", 609 | ] 610 | "#, 611 | ) 612 | .build(); 613 | 614 | // Act 615 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 616 | let cook_directory = TempDir::new().unwrap(); 617 | skeleton 618 | .build_minimum_project(cook_directory.path(), false) 619 | .unwrap(); 620 | 621 | // Assert 622 | let lock_file = skeleton.lock_file.expect("there should be a lock_file"); 623 | assert!(!lock_file.contains( 624 | r#" 625 | [[package]] 626 | name = "project-a" 627 | version = "1.2.3" 628 | "# 629 | )); 630 | assert!(lock_file.contains( 631 | r#" 632 | [[package]] 633 | name = "project-a" 634 | version = "0.0.1" 635 | "# 636 | )); 637 | assert!(!lock_file.contains( 638 | r#" 639 | [[package]] 640 | name = "project_b" 641 | version = "4.5.6" 642 | "# 643 | )); 644 | assert!(lock_file.contains( 645 | r#" 646 | [[package]] 647 | name = "project_b" 648 | version = "0.0.1" 649 | "# 650 | )); 651 | assert!(lock_file.contains( 652 | r#" 653 | [[package]] 654 | name = "either" 655 | version = "1.8.1" 656 | "# 657 | )); 658 | 659 | let first = skeleton.manifests[0].clone(); 660 | check( 661 | &first.contents, 662 | expect_test::expect![[r#" 663 | [workspace] 664 | members = ["src/project-a", "src/project_b"] 665 | "#]], 666 | ); 667 | let second = skeleton.manifests[1].clone(); 668 | check( 669 | &second.contents, 670 | expect_test::expect![[r#" 671 | [[bin]] 672 | path = "src/main.rs" 673 | name = "test-dummy" 674 | plugin = false 675 | proc-macro = false 676 | edition = "2018" 677 | required-features = [] 678 | 679 | [package] 680 | name = "project-a" 681 | edition = "2018" 682 | version = "0.0.1" 683 | 684 | [dependencies.either] 685 | version = "=1.8.1" 686 | "#]], 687 | ); 688 | let third = skeleton.manifests[2].clone(); 689 | check( 690 | &third.contents, 691 | expect_test::expect![[r#" 692 | [package] 693 | name = "project_b" 694 | edition = "2018" 695 | version = "0.0.1" 696 | 697 | [dependencies.either] 698 | version = "=1.8.1" 699 | 700 | [dependencies.project-a] 701 | version = "0.0.1" 702 | path = "../project-a" 703 | 704 | [lib] 705 | path = "src/lib.rs" 706 | name = "project_b" 707 | plugin = false 708 | proc-macro = false 709 | edition = "2018" 710 | required-features = [] 711 | crate-type = ["cdylib"] 712 | "#]], 713 | ); 714 | } 715 | 716 | #[test] 717 | pub fn transitive_workspace_dependency_not_masked() { 718 | // Arrange 719 | let project = CargoWorkspace::new() 720 | .manifest( 721 | ".", 722 | r#" 723 | [workspace] 724 | members = [ 725 | "src/project_a", 726 | "src/project_b", 727 | ] 728 | "#, 729 | ) 730 | .bin_package( 731 | "src/project_a", 732 | r#" 733 | [package] 734 | name = "project_a" 735 | version = "2.2.2" 736 | edition = "2018" 737 | 738 | [[bin]] 739 | name = "test-dummy" 740 | path = "src/main.rs" 741 | 742 | [dependencies] 743 | either = { version = "=1.8.1" } 744 | "#, 745 | ) 746 | .lib_package( 747 | "src/project_b", 748 | r#" 749 | [package] 750 | name = "project_b" 751 | version = "5.5.5" 752 | edition = "2018" 753 | 754 | [lib] 755 | crate-type = ["cdylib"] 756 | 757 | [dependencies] 758 | "#, 759 | ) 760 | .file( 761 | "Cargo.lock", 762 | r#" 763 | # This file is automatically @generated by Cargo. 764 | # It is not intended for manual editing. 765 | version = 3 766 | 767 | [[package]] 768 | name = "either" 769 | version = "1.8.1" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 772 | dependencies = [ 773 | "project_b 5.5.5 (registry+https://github.com/rust-lang/crates.io-index)", 774 | ] 775 | 776 | [[package]] 777 | name = "project_a" 778 | version = "2.2.2" 779 | dependencies = [ 780 | "either", 781 | ] 782 | 783 | [[package]] 784 | name = "project_b" 785 | version = "5.5.5" 786 | 787 | [[package]] 788 | name = "project_b" 789 | version = "5.5.5" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 792 | "#, 793 | ) 794 | .build(); 795 | 796 | // Act 797 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 798 | let cook_directory = TempDir::new().unwrap(); 799 | skeleton 800 | .build_minimum_project(cook_directory.path(), false) 801 | .unwrap(); 802 | 803 | // Assert 804 | let lock_file = skeleton.lock_file.expect("there should be a lock_file"); 805 | assert!(!lock_file.contains( 806 | r#" 807 | [[package]] 808 | name = "project_a" 809 | version = "2.2.2" 810 | "# 811 | )); 812 | assert!(lock_file.contains( 813 | r#" 814 | [[package]] 815 | name = "project_a" 816 | version = "0.0.1" 817 | "# 818 | )); 819 | assert!(lock_file.contains( 820 | r#" 821 | [[package]] 822 | name = "project_b" 823 | version = "5.5.5" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 826 | "# 827 | )); 828 | assert!(lock_file.contains( 829 | r#" 830 | [[package]] 831 | name = "project_b" 832 | version = "0.0.1" 833 | "# 834 | )); 835 | assert!(lock_file.contains( 836 | r#" 837 | [[package]] 838 | name = "either" 839 | version = "1.8.1" 840 | "# 841 | )); 842 | 843 | let first = skeleton.manifests[0].clone(); 844 | check( 845 | &first.contents, 846 | expect_test::expect![[r#" 847 | [workspace] 848 | members = ["src/project_a", "src/project_b"] 849 | "#]], 850 | ); 851 | let second = skeleton.manifests[1].clone(); 852 | check( 853 | &second.contents, 854 | expect_test::expect![[r#" 855 | [[bin]] 856 | path = "src/main.rs" 857 | name = "test-dummy" 858 | plugin = false 859 | proc-macro = false 860 | edition = "2018" 861 | required-features = [] 862 | 863 | [package] 864 | name = "project_a" 865 | edition = "2018" 866 | version = "0.0.1" 867 | 868 | [dependencies.either] 869 | version = "=1.8.1" 870 | "#]], 871 | ); 872 | let third = skeleton.manifests[2].clone(); 873 | check( 874 | &third.contents, 875 | expect_test::expect![[r#" 876 | [package] 877 | name = "project_b" 878 | edition = "2018" 879 | version = "0.0.1" 880 | 881 | [dependencies] 882 | 883 | [lib] 884 | path = "src/lib.rs" 885 | name = "project_b" 886 | plugin = false 887 | proc-macro = false 888 | edition = "2018" 889 | required-features = [] 890 | crate-type = ["cdylib"] 891 | "#]], 892 | ); 893 | } 894 | 895 | #[test] 896 | pub fn non_local_dependency_not_masked() { 897 | // Arrange 898 | let project = CargoWorkspace::new() 899 | .manifest( 900 | ".", 901 | r#" 902 | [workspace] 903 | members = [ 904 | "binary", 905 | "without", 906 | ] 907 | "#, 908 | ) 909 | .bin_package( 910 | "binary", 911 | r#" 912 | [package] 913 | name = "binary" 914 | version = "2.2.2" 915 | edition = "2021" 916 | 917 | [dependencies] 918 | without = "=0.1.0" 919 | without-local = { package = "without", path = "../without", version = "=0.1.0" } 920 | 921 | "#, 922 | ) 923 | .lib_package( 924 | "without", 925 | r#" 926 | [package] 927 | name = "without" 928 | version = "0.1.0" 929 | edition = "2021" 930 | 931 | [dependencies] 932 | 933 | "#, 934 | ) 935 | .file( 936 | "Cargo.lock", 937 | r#" 938 | # This file is automatically @generated by Cargo. 939 | # It is not intended for manual editing. 940 | version = 3 941 | 942 | [[package]] 943 | name = "binary" 944 | version = "2.2.2" 945 | dependencies = [ 946 | "without 0.1.0", 947 | "without 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 948 | ] 949 | 950 | [[package]] 951 | name = "without" 952 | version = "0.1.0" 953 | 954 | [[package]] 955 | name = "without" 956 | version = "0.1.0" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "3df10e9ed85b51fa3434bc5676eaa90479ce14ac3e101c8ce07e1bb5ef0b7255" 959 | 960 | "#, 961 | ) 962 | .build(); 963 | 964 | // Act 965 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 966 | let cook_directory = TempDir::new().unwrap(); 967 | skeleton 968 | .build_minimum_project(cook_directory.path(), false) 969 | .unwrap(); 970 | 971 | // Assert 972 | let lock_file = skeleton.lock_file.expect("there should be a lock_file"); 973 | println!("{}", lock_file); 974 | assert!(lock_file.contains( 975 | r#" 976 | [[package]] 977 | name = "binary" 978 | version = "0.0.1" 979 | dependencies = ["without 0.1.0", "without 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)"] 980 | 981 | "# 982 | )); 983 | assert!(lock_file.contains( 984 | r#" 985 | [[package]] 986 | name = "without" 987 | version = "0.0.1" 988 | "# 989 | )); 990 | assert!(lock_file.contains( 991 | r#" 992 | [[package]] 993 | name = "without" 994 | version = "0.1.0" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "3df10e9ed85b51fa3434bc5676eaa90479ce14ac3e101c8ce07e1bb5ef0b7255" 997 | "# 998 | )); 999 | 1000 | let first = skeleton.manifests[0].clone(); 1001 | check( 1002 | &first.contents, 1003 | expect_test::expect![[r#" 1004 | [workspace] 1005 | members = ["binary", "without"] 1006 | "#]], 1007 | ); 1008 | let second = skeleton.manifests[1].clone(); 1009 | check( 1010 | &second.contents, 1011 | expect_test::expect![[r#" 1012 | [[bin]] 1013 | path = "src/main.rs" 1014 | name = "binary" 1015 | plugin = false 1016 | proc-macro = false 1017 | edition = "2021" 1018 | required-features = [] 1019 | 1020 | [package] 1021 | name = "binary" 1022 | edition = "2021" 1023 | version = "0.0.1" 1024 | 1025 | [dependencies] 1026 | without = "=0.1.0" 1027 | 1028 | [dependencies.without-local] 1029 | version = "0.0.1" 1030 | path = "../without" 1031 | package = "without" 1032 | "#]], 1033 | ); 1034 | let third = skeleton.manifests[2].clone(); 1035 | check( 1036 | &third.contents, 1037 | expect_test::expect![[r#" 1038 | [package] 1039 | name = "without" 1040 | edition = "2021" 1041 | version = "0.0.1" 1042 | 1043 | [dependencies] 1044 | 1045 | [lib] 1046 | path = "src/lib.rs" 1047 | name = "without" 1048 | plugin = false 1049 | proc-macro = false 1050 | edition = "2021" 1051 | required-features = [] 1052 | crate-type = ["lib"] 1053 | "#]], 1054 | ); 1055 | } 1056 | 1057 | #[test] 1058 | pub fn ignore_vendored_directory() { 1059 | // Arrange 1060 | let project = CargoWorkspace::new() 1061 | .bin_package( 1062 | ".", 1063 | r#" 1064 | [package] 1065 | name = "test-dummy" 1066 | version = "1.2.3" 1067 | edition = "2018" 1068 | 1069 | [dependencies] 1070 | rocket = "0.5.0-rc.1" 1071 | "#, 1072 | ) 1073 | .file( 1074 | ".cargo/config.toml", 1075 | r#" 1076 | [source.crates-io] 1077 | replace-with = "vendored-sources" 1078 | 1079 | [source.vendored-sources] 1080 | directory = "vendor" 1081 | "#, 1082 | ) 1083 | .lib_package( 1084 | "vendor/rocket", 1085 | r#" 1086 | [package] 1087 | edition = "2018" 1088 | name = "rocket" 1089 | version = "0.5.0-rc.1" 1090 | authors = ["Sergio Benitez "] 1091 | build = "build.rs" 1092 | description = "Web framework with a focus on usability, security, extensibility, and speed.\n" 1093 | homepage = "https://rocket.rs" 1094 | documentation = "https://api.rocket.rs/v0.5-rc/rocket/" 1095 | readme = "../../README.md" 1096 | keywords = ["rocket", "web", "framework", "server"] 1097 | categories = ["web-programming::http-server"] 1098 | license = "MIT OR Apache-2.0" 1099 | repository = "https://github.com/SergioBenitez/Rocket" 1100 | 1101 | [package.metadata.docs.rs] 1102 | all-features = true 1103 | 1104 | [dependencies.rocket_dep] 1105 | version = "0.3.2" 1106 | "#, 1107 | ) 1108 | .file( 1109 | "vendor/rocket/.cargo-checksum.json", 1110 | r#" 1111 | {"files": {}} 1112 | "#, 1113 | ) 1114 | .lib_package( 1115 | "vendor/rocket_dep", 1116 | r#" 1117 | [package] 1118 | edition = "2018" 1119 | name = "rocket_dep" 1120 | version = "0.3.2" 1121 | authors = ["Test author"] 1122 | description = "sample package representing all of rocket's dependencies" 1123 | "#, 1124 | ) 1125 | .file( 1126 | "vendor/rocket_dep/.cargo-checksum.json", 1127 | r#" 1128 | {"files": {}} 1129 | "#, 1130 | ) 1131 | .build(); 1132 | 1133 | // Act 1134 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 1135 | 1136 | // Assert 1137 | assert_eq!(1, skeleton.manifests.len()); 1138 | } 1139 | 1140 | #[test] 1141 | pub fn specify_member_in_workspace() { 1142 | // Arrange 1143 | let project = CargoWorkspace::new() 1144 | .manifest( 1145 | ".", 1146 | r#" 1147 | [workspace] 1148 | members = [ 1149 | "backend", 1150 | "ci", 1151 | ] 1152 | "#, 1153 | ) 1154 | .bin_package( 1155 | "backend", 1156 | r#" 1157 | [package] 1158 | name = "backend" 1159 | version = "0.1.0" 1160 | edition = "2018" 1161 | "#, 1162 | ) 1163 | .bin_package( 1164 | "ci", 1165 | r#" 1166 | [package] 1167 | name = "ci" 1168 | version = "0.1.0" 1169 | edition = "2018" 1170 | "#, 1171 | ) 1172 | .build(); 1173 | 1174 | // Act 1175 | let skeleton = Skeleton::derive(project.path(), "backend".to_string().into()).unwrap(); 1176 | 1177 | // Assert: 1178 | // - that "ci" is *still* in the list of `skeleton`'s manifests 1179 | assert!(skeleton 1180 | .manifests 1181 | .iter() 1182 | .any(|manifest| !manifest.contents.contains("ci"))); 1183 | 1184 | // - that the list of members has been cut down to "backend", as expected 1185 | let gold = r#"[workspace] 1186 | members = ["backend"] 1187 | "#; 1188 | assert!( 1189 | skeleton 1190 | .manifests 1191 | .iter() 1192 | .find(|manifest| manifest.relative_path == std::path::PathBuf::from("Cargo.toml")) 1193 | .unwrap() 1194 | .contents 1195 | == gold 1196 | ); 1197 | } 1198 | 1199 | #[test] 1200 | pub fn mask_workspace_dependencies() { 1201 | // Arrange 1202 | let project = CargoWorkspace::new() 1203 | .manifest( 1204 | ".", 1205 | r#" 1206 | [workspace] 1207 | members = [ 1208 | "project_a", 1209 | "project_b", 1210 | ] 1211 | 1212 | [workspace.package] 1213 | version = "0.2.0" 1214 | edition = "2021" 1215 | license = "Apache-2.0" 1216 | 1217 | [workspace.dependencies] 1218 | anyhow = "1.0.66" 1219 | project_a = { path = "project_a", version = "0.2.0" } 1220 | "#, 1221 | ) 1222 | .bin_package( 1223 | "project_a", 1224 | r#" 1225 | [package] 1226 | name = "project_a" 1227 | version.workspace = true 1228 | edition.workspace = true 1229 | license.workspace = true 1230 | 1231 | [dependencies] 1232 | anyhow = { workspace = true } 1233 | "#, 1234 | ) 1235 | .lib_package( 1236 | "project_b", 1237 | r#" 1238 | [package] 1239 | name = "project_b" 1240 | version.workspace = true 1241 | edition.workspace = true 1242 | license.workspace = true 1243 | 1244 | [lib] 1245 | crate-type = ["cdylib"] 1246 | 1247 | [dependencies] 1248 | project_a = { workspace = true } 1249 | anyhow = { workspace = true } 1250 | "#, 1251 | ) 1252 | .build(); 1253 | 1254 | // Act 1255 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 1256 | let cook_directory = TempDir::new().unwrap(); 1257 | skeleton 1258 | .build_minimum_project(cook_directory.path(), false) 1259 | .unwrap(); 1260 | 1261 | let first = skeleton.manifests[0].clone(); 1262 | check( 1263 | &first.contents, 1264 | expect_test::expect![[r#" 1265 | [workspace] 1266 | members = ["project_a", "project_b"] 1267 | 1268 | [workspace.dependencies] 1269 | anyhow = "1.0.66" 1270 | 1271 | [workspace.dependencies.project_a] 1272 | version = "0.0.1" 1273 | path = "project_a" 1274 | 1275 | [workspace.package] 1276 | edition = "2021" 1277 | version = "0.0.1" 1278 | license = "Apache-2.0" 1279 | "#]], 1280 | ); 1281 | 1282 | let second = skeleton.manifests[1].clone(); 1283 | check( 1284 | &second.contents, 1285 | expect_test::expect![[r#" 1286 | [[bin]] 1287 | path = "src/main.rs" 1288 | name = "project_a" 1289 | plugin = false 1290 | proc-macro = false 1291 | required-features = [] 1292 | 1293 | [package] 1294 | name = "project_a" 1295 | 1296 | [package.edition] 1297 | workspace = true 1298 | 1299 | [package.version] 1300 | workspace = true 1301 | 1302 | [package.license] 1303 | workspace = true 1304 | 1305 | [dependencies.anyhow] 1306 | workspace = true 1307 | "#]], 1308 | ); 1309 | 1310 | let third = skeleton.manifests[2].clone(); 1311 | check( 1312 | &third.contents, 1313 | expect_test::expect![[r#" 1314 | [package] 1315 | name = "project_b" 1316 | 1317 | [package.edition] 1318 | workspace = true 1319 | 1320 | [package.version] 1321 | workspace = true 1322 | 1323 | [package.license] 1324 | workspace = true 1325 | 1326 | [dependencies.anyhow] 1327 | workspace = true 1328 | 1329 | [dependencies.project_a] 1330 | workspace = true 1331 | 1332 | [lib] 1333 | path = "src/lib.rs" 1334 | name = "project_b" 1335 | plugin = false 1336 | proc-macro = false 1337 | required-features = [] 1338 | crate-type = ["cdylib"] 1339 | "#]], 1340 | ); 1341 | } 1342 | 1343 | #[test] 1344 | pub fn workspace_glob_members() { 1345 | // Arrange 1346 | let project = CargoWorkspace::new() 1347 | .manifest( 1348 | ".", 1349 | r#" 1350 | [workspace] 1351 | members = ["crates/*"] 1352 | "#, 1353 | ) 1354 | .bin_package( 1355 | "crates/project_a", 1356 | r#" 1357 | [package] 1358 | name = "project_a" 1359 | version = "0.0.1" 1360 | "#, 1361 | ) 1362 | .lib_package( 1363 | "crates/project_b", 1364 | r#" 1365 | [package] 1366 | name = "project_b" 1367 | version = "0.0.1" 1368 | "#, 1369 | ) 1370 | .lib_package( 1371 | "crates-unused/project_c", 1372 | r#" 1373 | [package] 1374 | name = "project_c" 1375 | version = "0.0.1" 1376 | "#, 1377 | ) 1378 | .build(); 1379 | 1380 | // Act 1381 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 1382 | 1383 | // Assert 1384 | assert_eq!(skeleton.manifests.len(), 3); 1385 | } 1386 | 1387 | #[test] 1388 | pub fn renamed_local_dependencies() { 1389 | // Arrange 1390 | let project = CargoWorkspace::new() 1391 | .manifest( 1392 | ".", 1393 | r#" 1394 | [workspace] 1395 | members = ["a", "b"] 1396 | "#, 1397 | ) 1398 | .lib_package( 1399 | "a", 1400 | r#" 1401 | [package] 1402 | name = "a" 1403 | version = "0.5.0" 1404 | 1405 | [dependencies.c] 1406 | version = "0.2.1" 1407 | package = "b" 1408 | path = "../b" 1409 | "#, 1410 | ) 1411 | .lib_package( 1412 | "b", 1413 | r#" 1414 | [package] 1415 | name = "b" 1416 | version = "0.2.1" 1417 | "#, 1418 | ) 1419 | .build(); 1420 | 1421 | // Act 1422 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 1423 | 1424 | check( 1425 | &skeleton.manifests[1].contents, 1426 | expect![[r#" 1427 | [package] 1428 | name = "a" 1429 | version = "0.0.1" 1430 | 1431 | [dependencies.c] 1432 | version = "0.0.1" 1433 | path = "../b" 1434 | package = "b" 1435 | 1436 | [lib] 1437 | path = "src/lib.rs" 1438 | name = "a" 1439 | plugin = false 1440 | proc-macro = false 1441 | required-features = [] 1442 | crate-type = ["lib"] 1443 | "#]], 1444 | ); 1445 | } 1446 | 1447 | #[test] 1448 | pub fn rust_toolchain() { 1449 | // Arrange 1450 | let project = CargoWorkspace::new() 1451 | .manifest( 1452 | ".", 1453 | r#" 1454 | [package] 1455 | name = "test-dummy" 1456 | version = "0.1.0" 1457 | edition = "2021" 1458 | 1459 | [dependencies] 1460 | "#, 1461 | ) 1462 | .touch("src/main.rs") 1463 | .touch("Cargo.lock") 1464 | .file("rust-toolchain", "1.75.0") 1465 | .build(); 1466 | 1467 | // Act 1468 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 1469 | let cook_directory = TempDir::new().unwrap(); 1470 | skeleton 1471 | .build_minimum_project(cook_directory.path(), false) 1472 | .unwrap(); 1473 | 1474 | // Assert 1475 | assert_eq!(1, skeleton.manifests.len()); 1476 | let manifest = &skeleton.manifests[0]; 1477 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 1478 | cook_directory 1479 | .child("src") 1480 | .child("main.rs") 1481 | .assert("fn main() {}"); 1482 | cook_directory 1483 | .child("Cargo.lock") 1484 | .assert(predicate::path::exists()); 1485 | cook_directory.child("rust-toolchain").assert("1.75.0"); 1486 | } 1487 | 1488 | #[test] 1489 | pub fn rust_toolchain_toml() { 1490 | // Arrange 1491 | let project = CargoWorkspace::new() 1492 | .manifest( 1493 | ".", 1494 | r#" 1495 | [package] 1496 | name = "test-dummy" 1497 | version = "0.1.0" 1498 | edition = "2021" 1499 | 1500 | [dependencies] 1501 | "#, 1502 | ) 1503 | .touch("src/main.rs") 1504 | .touch("Cargo.lock") 1505 | .file( 1506 | "rust-toolchain.toml", 1507 | r#" 1508 | [toolchain] 1509 | channel = "1.75.0" 1510 | "#, 1511 | ) 1512 | .build(); 1513 | 1514 | // Act 1515 | let skeleton = Skeleton::derive(project.path(), None).unwrap(); 1516 | let cook_directory = TempDir::new().unwrap(); 1517 | skeleton 1518 | .build_minimum_project(cook_directory.path(), false) 1519 | .unwrap(); 1520 | 1521 | // Assert 1522 | assert_eq!(1, skeleton.manifests.len()); 1523 | let manifest = &skeleton.manifests[0]; 1524 | assert_eq!(Path::new("Cargo.toml"), manifest.relative_path); 1525 | cook_directory 1526 | .child("src") 1527 | .child("main.rs") 1528 | .assert("fn main() {}"); 1529 | cook_directory 1530 | .child("Cargo.lock") 1531 | .assert(predicate::path::exists()); 1532 | cook_directory.child("rust-toolchain.toml").assert( 1533 | r#"[toolchain] 1534 | channel = "1.75.0" 1535 | "#, 1536 | ); 1537 | } 1538 | fn check(actual: &str, expect: Expect) { 1539 | let actual = actual.to_string(); 1540 | expect.assert_eq(&actual); 1541 | } 1542 | 1543 | #[derive(Default)] 1544 | struct CargoWorkspace { 1545 | files: HashMap, 1546 | } 1547 | impl CargoWorkspace { 1548 | fn new() -> Self { 1549 | Self::default() 1550 | } 1551 | 1552 | fn manifest>(&mut self, directory: P, content: &str) -> &mut Self { 1553 | self.file(directory.as_ref().join("Cargo.toml"), content) 1554 | } 1555 | 1556 | fn lib_package>(&mut self, directory: P, content: &str) -> &mut Self { 1557 | let directory = directory.as_ref(); 1558 | self.manifest(directory, content) 1559 | .file(directory.join("src/lib.rs"), "") 1560 | } 1561 | 1562 | fn bin_package>(&mut self, directory: P, content: &str) -> &mut Self { 1563 | let directory = directory.as_ref(); 1564 | self.manifest(directory, content) 1565 | .file(directory.join("src/main.rs"), "") 1566 | } 1567 | 1568 | fn file>(&mut self, path: P, content: &str) -> &mut Self { 1569 | let path = PathBuf::from(path.as_ref()); 1570 | 1571 | assert!(self.files.insert(path, content.to_string()).is_none()); 1572 | self 1573 | } 1574 | 1575 | fn touch>(&mut self, path: P) -> &mut Self { 1576 | self.file(path, "") 1577 | } 1578 | fn touch_multiple>(&mut self, paths: &[P]) -> &mut Self { 1579 | for path in paths { 1580 | self.touch(path); 1581 | } 1582 | self 1583 | } 1584 | 1585 | fn build(&mut self) -> BuiltWorkspace { 1586 | let directory = TempDir::new().unwrap(); 1587 | for (file, content) in &self.files { 1588 | let path = directory.join(file); 1589 | let content = content.trim_start(); 1590 | std::fs::create_dir_all(path.parent().unwrap()).unwrap(); 1591 | std::fs::write(path, content).unwrap(); 1592 | } 1593 | BuiltWorkspace { directory } 1594 | } 1595 | } 1596 | 1597 | /// See https://github.com/LukeMathWalker/cargo-chef/issues/232. 1598 | #[test] 1599 | pub fn workspace_bin_nonstandard_dirs() { 1600 | // Arrange 1601 | let project = CargoWorkspace::new() 1602 | .manifest( 1603 | ".", 1604 | r#" 1605 | [workspace] 1606 | members = [ 1607 | "crates/client/project_a", 1608 | "crates/client/project_b", 1609 | "crates/server/*", 1610 | "vendored/project_e", 1611 | "project_f", 1612 | ] 1613 | "#, 1614 | ) 1615 | .bin_package( 1616 | "crates/client/project_a", 1617 | r#" 1618 | [package] 1619 | name = "project_a" 1620 | version = "0.1.0" 1621 | edition = "2018" 1622 | 1623 | [dependencies] 1624 | uuid = { version = "=0.8.0", features = ["v4"] } 1625 | "#, 1626 | ) 1627 | .bin_package( 1628 | "crates/client/project_b", 1629 | r#" 1630 | [package] 1631 | name = "project_b" 1632 | version = "0.1.0" 1633 | edition = "2018" 1634 | 1635 | [dependencies] 1636 | uuid = { version = "=0.8.0", features = ["v4"] } 1637 | "#, 1638 | ) 1639 | .bin_package( 1640 | "crates/server/project_c", 1641 | r#" 1642 | [package] 1643 | name = "project_c" 1644 | version = "0.1.0" 1645 | edition = "2018" 1646 | 1647 | [dependencies] 1648 | uuid = { version = "=0.8.0", features = ["v4"] } 1649 | "#, 1650 | ) 1651 | .bin_package( 1652 | "crates/server/project_d", 1653 | r#" 1654 | [package] 1655 | name = "project_d" 1656 | version = "0.1.0" 1657 | edition = "2018" 1658 | 1659 | [dependencies] 1660 | uuid = { version = "=0.8.0", features = ["v4"] } 1661 | "#, 1662 | ) 1663 | .bin_package( 1664 | "vendored/project_e", 1665 | r#" 1666 | [package] 1667 | name = "project_e" 1668 | version = "0.1.0" 1669 | edition = "2018" 1670 | 1671 | [dependencies] 1672 | uuid = { version = "=0.8.0", features = ["v4"] } 1673 | "#, 1674 | ) 1675 | .bin_package( 1676 | "project_f", 1677 | r#" 1678 | [package] 1679 | name = "project_f" 1680 | version = "0.1.0" 1681 | edition = "2018" 1682 | 1683 | [dependencies] 1684 | uuid = { version = "=0.8.0", features = ["v4"] } 1685 | "#, 1686 | ) 1687 | .build(); 1688 | 1689 | fn manifest_content_dirs(skeleton: &Skeleton) -> Vec { 1690 | // This is really ugly... sorry. 1691 | skeleton 1692 | .manifests 1693 | .first() 1694 | .unwrap() 1695 | .contents 1696 | .split('=') 1697 | .last() 1698 | .unwrap() 1699 | .replace(['[', ']', '"'], "") 1700 | .trim() 1701 | .split(',') 1702 | .map(|w| w.trim().to_string()) 1703 | .collect() 1704 | } 1705 | 1706 | // Act 1707 | let path = project.path(); 1708 | let all = Skeleton::derive(&path, None).unwrap(); 1709 | assert_eq!( 1710 | manifest_content_dirs(&all), 1711 | vec![ 1712 | "crates/client/project_a", 1713 | "crates/client/project_b", 1714 | "crates/server/*", 1715 | "vendored/project_e", 1716 | "project_f" 1717 | ] 1718 | ); 1719 | 1720 | let project_a = Skeleton::derive(&path, Some("project_a".into())).unwrap(); 1721 | assert_eq!( 1722 | manifest_content_dirs(&project_a), 1723 | vec!["crates/client/project_a"] 1724 | ); 1725 | 1726 | let project_b = Skeleton::derive(&path, Some("project_b".into())).unwrap(); 1727 | assert_eq!( 1728 | manifest_content_dirs(&project_b), 1729 | vec!["crates/client/project_b"] 1730 | ); 1731 | 1732 | let project_c = Skeleton::derive(&path, Some("project_c".into())).unwrap(); 1733 | assert_eq!( 1734 | manifest_content_dirs(&project_c), 1735 | vec!["crates/server/project_c"] 1736 | ); 1737 | 1738 | let project_d = Skeleton::derive(&path, Some("project_d".into())).unwrap(); 1739 | assert_eq!( 1740 | manifest_content_dirs(&project_d), 1741 | vec!["crates/server/project_d"] 1742 | ); 1743 | 1744 | let project_e = Skeleton::derive(&path, Some("project_e".into())).unwrap(); 1745 | assert_eq!( 1746 | manifest_content_dirs(&project_e), 1747 | vec!["vendored/project_e"] 1748 | ); 1749 | 1750 | let project_f = Skeleton::derive(&path, Some("project_f".into())).unwrap(); 1751 | assert_eq!(manifest_content_dirs(&project_f), vec!["project_f"]); 1752 | 1753 | // TODO: If multiple binaries are valid in `cargo chef prepare`, then testing 1754 | // with multiple binaries is probably a good idea here! 1755 | } 1756 | 1757 | struct BuiltWorkspace { 1758 | directory: TempDir, 1759 | } 1760 | impl BuiltWorkspace { 1761 | fn path(&self) -> PathBuf { 1762 | self.directory.canonicalize().unwrap() 1763 | } 1764 | } 1765 | --------------------------------------------------------------------------------