Rust is a great programming language, but it’s compiler is known to be slow. The main reason is that it checks so many things during the compilation in order to provides memory and concurrency safety. Like in many other cases, pipelines may be speeded up by caching, but it is very important to set the caching properly, otherwise it won’t help. Besides compiling your code may be slow, you also may want to use some cargo tools to lint and check your code and this will require to be compiled as well, if you don’t want to download binaries. Some popular tools are:

I keep all my projects at GitHub, both public and private and I like the features of GitHub Actions . They support caching and for this purpose I use actions/cache . Fortunately, there are actions/cache snippets for all major languages including Rust . Before I implemented this action properly, my caching didn’t work so I had lint, check and test actions runtime at up to 15 minutes. After fixing it, all of these steps last just ~1 minute. But let’s show the reusable workflow that I use:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
on:
  workflow_call:
    inputs:
      rust-version:
        type: string
        required: false
        default: nightly

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: Check out
        uses: actions/checkout@v3
      - name: Install Rust
        uses: actions-rs/toolchain@v1
        with:
          profile: minimal
          toolchain: ${{ inputs.rust-version }}
          override: true
          components: rustfmt, clippy
      - name: Set up cargo cache
        uses: actions/cache@v3
        continue-on-error: false
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            target/            
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ${{ runner.os }}-cargo-
      - name: Lint
        run: |
          cargo fmt --all -- --check
          cargo clippy -- -D warnings          
      - name: Install cargo check tools
        run: |
          cargo install --locked cargo-deny || true
          cargo install --locked cargo-outdated || true
          cargo install --locked cargo-udeps || true
          cargo install --locked cargo-audit || true
          cargo install --locked cargo-pants || true          
      - name: Check
        run: |
          cargo deny check
          cargo outdated --exit-code 1
          cargo udeps
          rm -rf ~/.cargo/advisory-db
          cargo audit
          cargo pants          
      - name: Test
        run: cargo test

As you can see, I just used the recommended setup for actions/cache but then some cargo install commands used to fail in subsequent runs when binary is already installed in ~/.cargo/bin. So this is why I had to ignore these errors by adding || true after each cargo install command. There is also some funny error related to the cached advisory-db, but as temporary solution I just delete it before running cargo audit and cargo pants. I will address it properly at some time later.

This workflow can be invoked from any Rust project by:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
name: pipeline

on:
  push:
  pull_request:
  workflow_dispatch:

jobs:
  check:
    uses: ectobit/reusable-workflows/.github/workflows/rust-check.yaml@main

But this is not all, what about the build, actually specifically docker build? By using registry type of caching in docker/build-push-action , I speeded up the builds from ~50 to ~2 minutes. At the end, my project’s pipeline speeded up from ~65 minutes in total to just ~2 minutes. Yeah, this is ~30 times faster, great. Again, here is the reusable workflow which may be used by other projects as well, not just Rust:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
on:
  workflow_call:
    inputs:
      image:
        type: string
        required: true
      platforms:
        type: string
        required: false
        default: linux/amd64,linux/arm64
      license:
        type: string
        required: false
        default: BSD-2-Clause-Patent
      vendor:
        type: string
        required: false
        default: ectobit.com
      hadolint-dockerfile:
        type: string
        required: false
        default: Dockerfile
      hadolint-ignore:
        type: string
        required: false
        default: ''
      build-args:
        type: string
        required: false
        default: ''
    secrets:
      container-registry-username:
        required: true
      container-registry-password:
        required: true
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out
        uses: actions/checkout@v3
      - name: Lint Dockerfile with ignore
        if: ${{ inputs.hadolint-ignore != ''}}
        uses: hadolint/hadolint-action@v2.1.0
        with:
          dockerfile: ${{ inputs.hadolint-dockerfile }}
          ignore: ${{ inputs.hadolint-ignore }}
      - name: Lint Dockerfile
        if: ${{ inputs.hadolint-ignore == ''}}
        uses: hadolint/hadolint-action@v2.1.0
        with:
          dockerfile: ${{ inputs.hadolint-dockerfile }}
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2.0.0
      - name: Set up tags
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ inputs.image }}
          labels: |
            org.opencontainers.image.licenses=${{ inputs.license }}
            org.opencontainers.image.vendor=${{ inputs.vendor }}            
          tags: |
            type=schedule
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=edge
            type=sha            
      - name: Login to container registry
        uses: docker/login-action@v2.0.0
        with:
          username: ${{ secrets.container-registry-username }}
          password: ${{ secrets.container-registry-password }}
      - name: Build and push image with build args to container registry
        uses: docker/build-push-action@v3.0.0
        if: ${{ inputs.build-args != ''}}
        with:
          build-args: ${{ inputs.build-args }}
          platforms: ${{ inputs.platforms }}
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=registry,ref=${{ inputs.image }}:buildcache
          cache-to: type=registry,ref=${{ inputs.image }}:buildcache,mode=max
      - name: Build and push image to container registry
        uses: docker/build-push-action@v3.0.0
        if: ${{ inputs.build-args == ''}}
        with:
          platforms: ${{ inputs.platforms }}
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=registry,ref=${{ inputs.image }}:buildcache
          cache-to: type=registry,ref=${{ inputs.image }}:buildcache,mode=max

This can be invoked in your project by:

1
2
3
4
5
6
7
8
build:
  uses: ectobit/reusable-workflows/.github/workflows/buildx.yaml@main
  needs: check
  with:
    image: ectobit/bond
  secrets:
    container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
    container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}

That’s all folks, you may try to use this in your projects and have faster build times.