Post

Rust Build with Cargo: Build Files, Scanners and CI

How Cargo builds, tests and secures Rust projects, which files it needs, how to lint and audit dependencies, and a complete GitHub Actions pipeline.

Rust Build with Cargo: Build Files, Scanners and CI

Cargo is Rust’s build tool, package manager, test runner, benchmark runner and documentation generator in one. Combined with rustup for toolchains and a small set of community subcommands (cargo-audit, cargo-deny, cargo-llvm-cov), it covers the whole build pipeline with very little glue.

This post is part of Build Ecosystems by Programming Language. Cross-compilation, publishing and deployment patterns are covered in the Rust Build and Deployment Tools Guide.

At a glance

CategoryCommon tools
Build toolsCargo, rustup, cargo-make, cross (cross-compilation in containers), cargo-zigbuild, cargo-dist, maturin (Python extensions), Bazel (rules_rust)
Package registrycrates.io, private registries (Artifactory, Cloudsmith, Kellnr)
Build filesCargo.toml, Cargo.lock, rust-toolchain.toml, .cargo/config.toml, build.rs, rustfmt.toml, clippy.toml, deny.toml
Lint and formatrustfmt, Clippy
SAST (open source)Clippy, Semgrep, CodeQL, cargo-geiger (counts unsafe)
SAST (commercial)Snyk Code, Checkmarx One, Veracode
SCAcargo-audit (RustSec), cargo-deny (advisories, licences, bans, sources), OSV-Scanner, Trivy, Snyk, Mend
FrameworksTokio, Axum, Actix Web, Rocket, Tauri, Leptos, clap (CLIs)
Test and coveragecargo test, cargo-nextest, proptest, criterion (benchmarks); cargo-llvm-cov, cargo-tarpaulin
Build outputNative binary, static library, .crate package, Python wheel (maturin)
CI cache~/.cargo/registry, ~/.cargo/git, target/ (via Swatinem/rust-cache) or sccache

How the build works

In CI, each stage of the generic build pipeline maps to a Cargo command, plugin or scanner. Press play, or step through it:

  1. Source + Cargo.toml: A commit triggers CI. Cargo.toml declares dependencies, Cargo.lock pins them and rust-toolchain.toml pins the compiler.
  2. Fetch crates: Cargo downloads crates from crates.io or a private registry; --locked fails on a stale Cargo.lock, and rust-cache caches ~/.cargo and target/.
  3. Lint and format: cargo fmt --all --check fails on unformatted code, and cargo clippy -- -D warnings turns every lint into an error.
  4. Compile: cargo build --release --locked compiles an optimised native binary.
  5. Test and coverage: cargo nextest run (or cargo test) runs the tests, and cargo llvm-cov --fail-under-lines 80 enforces coverage.
  6. Security scans in parallel: Semgrep checks our code, cargo audit checks the RustSec database, cargo deny checks advisories and licences, and Gitleaks scans for secrets.
  7. Quality gate: Format, Clippy, tests, the coverage gate and the supply-chain checks must pass before release.
  8. Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
  9. Package the crate (pass): cargo publish --dry-run builds and validates the .crate package; the release binary is the other output.
  10. SBOM and signing (pass): cargo cyclonedx --format json writes the SBOM, and the artifacts are signed with Sigstore cosign.
  11. Publish (pass): cargo publish uploads the crate to crates.io or a private registry; binaries are kept as CI artifacts.
  • Profiles control optimisation: dev (fast compile, debug info) and release (opt-level = 3); tune lto and codegen-units for smaller, faster release binaries.
  • Features turn optional code and dependencies on or off; CI should build with the feature sets you ship (--all-features, --no-default-features).
  • build.rs runs before compiling the crate (code generation, linking native libraries); it executes arbitrary code at build time, so review it in dependencies.
  • --locked makes Cargo fail instead of updating Cargo.lock, which keeps CI reproducible.

Build files

FilePurposeCommit?
Cargo.tomlPackage metadata, dependencies, features, profiles, workspace membersYes
Cargo.lockExact dependency versions and checksumsYes (for binaries and, nowadays, libraries too)
rust-toolchain.tomlPinned Rust version, components (clippy, rustfmt), targetsYes
.cargo/config.tomlRegistry mirrors, linker, target flags, aliasesYes, without tokens
build.rsBuild scriptYes
rustfmt.toml, clippy.tomlFormat and lint settingsYes
deny.tomlcargo-deny policy (allowed licences, banned crates, advisory ignores)Yes
target/Build outputNo

Commands by category

1
2
3
4
5
6
7
8
9
cargo build --release --locked                   # build
cargo fmt --all --check                          # format check
cargo clippy --all-targets --all-features -- -D warnings   # lint as a gate
cargo audit                                      # SCA (RustSec advisory database)
cargo deny check                                 # advisories + licences + bans + sources
cargo nextest run                                # tests (faster runner); or cargo test
cargo llvm-cov --fail-under-lines 80             # coverage gate
cargo cyclonedx --format json                    # SBOM (cargo-cyclonedx)
cargo publish --dry-run                          # validate the crate package

Minimal project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Cargo.toml
[package]
name = "demo"
version = "1.0.0"
edition = "2024"
rust-version = "1.85"

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

[profile.release]
lto = true
codegen-units = 1
strip = true
1
2
3
4
# rust-toolchain.toml
[toolchain]
channel = "1.85"
components = ["clippy", "rustfmt"]
1
2
3
4
5
6
7
8
9
10
# deny.toml (minimal cargo-deny policy)
[advisories]
version = 2

[licenses]
version = 2
allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "ISC", "Unicode-3.0"]

[bans]
multiple-versions = "warn"

Frameworks

FrameworkStart a project
Axumcargo new api && cd api && cargo add axum tokio -F tokio/full
Actix Webcargo new api && cd api && cargo add actix-web
Tauri (desktop)cargo install create-tauri-app && cargo create-tauri-app
clap (CLI)cargo new cli && cd cli && cargo add clap -F derive

CI with GitHub Actions

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
name: rust-ci
on: [push, pull_request]
permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt
      - uses: Swatinem/rust-cache@v2

      - run: cargo fmt --all --check
      - run: cargo clippy --all-targets --all-features --locked -- -D warnings
      - run: cargo test --locked
      - uses: EmbarkStudios/cargo-deny-action@v2   # advisories, licences, bans
      - run: cargo build --release --locked

      - uses: actions/upload-artifact@v4
        with:
          name: demo
          path: target/release/demo

Best practices

  • Pin the toolchain in rust-toolchain.toml and set rust-version (MSRV) in Cargo.toml.
  • Build with --locked in CI and commit Cargo.lock.
  • Make Clippy a gate with -D warnings; allow specific lints in code with a reason rather than disabling them globally.
  • Use cargo-deny as the single SCA and licence policy, and run cargo audit on a schedule so new advisories surface even without code changes.
  • Cache wisely: Swatinem/rust-cache or sccache; target/ grows large, so prune old artifacts.
  • Strip and optimise release binaries (strip = true, lto, codegen-units = 1) and build with musl or in distroless images for small containers.
This post is licensed under CC BY 4.0 by the author.