Skip to content
intermediate Phase 16 · Systems Projects and Interview Preparation

Production Crate Baseline

Set up CI lint gates, MSRV policy, and cross-compilation for releases.

40m
0 problems
Topic Progress 0%

The Four Commands That Turn 'Works on My Machine' Into a Merge Gate

A pull request goes green on a laptop: the author ran cargo build, it compiled, they pushed. Twenty minutes later CI is red — or worse, CI is green and the crate is published with a clippy warning every downstream user will see. The fix is not discipline, it is automation: every push must run the same four checks a careful human would run, and nothing merges without them. In Rust those checks are cargo fmt --check, cargo clippy -- -D warnings, cargo test, and cargo audit, and together they take minutes to set up.

Start with formatting. cargo fmt uses rustfmt's defaults, which are stable across machines, so there is no style debate left to have. The mistake is running cargo fmt in CI and letting it rewrite files — CI must check instead and fail on any diff:

cargo fmt --all -- --check

That exit code is the gate. Contributors run cargo fmt locally before pushing; CI proves they did.

Next, linting. cargo clippy ships with the toolchain and catches the mistakes rustc accepts: clone_on_copy, redundant_closure, needless_return, manual_map. The decision that makes it a gate instead of a suggestion is denying warnings:

cargo clippy --all-targets --all-features -- -D warnings

--all-targets lints tests, benches, and examples too — code that otherwise rots outside the lint umbrella. -D warnings promotes every warning to an error, which means a new clippy lint shipped in a toolchain update breaks the build until someone acknowledges it. Teams that fear that flip-flop: deny warnings in CI only, and keep warn locally, so developers see new lints without their day stalling. Project-level lint policy belongs in Cargo.toml where it travels with the source:

[lints.rust]
unsafe_code = "forbid"

[lints.clippy]
unwrap_used = "warn"
pedantic = { level = "warn", priority = -1 }

The priority = -1 on the pedantic group lets you override individual pedantic lints below it without tripping Cargo's "unused lint level" warning. A clippy.toml next to it tunes lint behavior — msrv = "1.75", avoid-breaking-exported-api = true — without changing which lints run.

Third, tests: cargo test runs unit tests, doc tests, and integration tests in one command. Doc tests matter more than teams expect — the code snippets in your /// docs are compiled and executed, so an example that stops compiling fails CI exactly when the docs went stale.

A minimal GitHub Actions job that runs all three looks like this:

name: ci
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
      - uses: Swatinem/rust-cache@v2
      - run: cargo fmt --all -- --check
      - run: cargo clippy --all-targets --all-features -- -D warnings
      - run: cargo test --all-features

Swatinem/rust-cache caches target/ and the registry between runs; without it a large workspace can spend more time rebuilding dependencies than running your tests. The toolchain action pins what CI compiles with — the same value you will declare as your MSRV policy in the next chapter. Keep total feedback under about ten minutes: split jobs per target or feature set if you cross it, because a gate nobody waits for is a gate people route around.

Interviewers ask about CI as a proxy for engineering maturity: "how do you prevent regressions in a Rust codebase?" The strong answer names the commands and the reasons — rustfmt removes style churn from review, clippy with -D warnings catches whole classes of bugs pre-merge, doc tests keep examples honest, and caching keeps the gate fast enough to respect. For how profiling fits into the same pipeline after a release ships, see Profiling with Flamegraphs and Perf.

Telling Consumers Which Compiler They Can Trust: MSRV

You publish acme-parse 1.2.0. A user on a distribution-pinned Rust 1.70 tries to build it and gets a wall of errors about stabilized features. They file an issue titled "1.2.0 is broken" — and they are right, because nothing told them which compiler the crate supports. The minimum supported Rust version (MSRV) is the oldest compiler your crate is guaranteed to build with, and a crate that does not document one leaves every consumer guessing.

Declaring it takes one line in Cargo.toml:

[package]
name = "acme-parse"
version = "1.2.0"
rust-version = "1.75"

That field is metadata, not enforcement — Cargo reads it and refuses to build with anything older, printing rustc 1.68.0 is not supported by the following package: acme-parse requires rustc 1.75 or newer. Before that refusal existed, an old compiler produced confusing syntax errors; now the failure names the real problem in one line. What the field cannot do is test the claim. For that, pin your CI's MSRV job to that exact version and run the suite against it:

  msrv:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@1.75.0
      - uses: Swatinem/rust-cache@v2
      - run: cargo check --all-features

Note the deliberate choice of cargo check over cargo test: the MSRV job proves the code compiles on the old compiler, while the stable job runs the tests. Testing on both is more thorough but doubles minutes; check-only is the common trade for library crates. For an exact local answer — which release introduced the feature that broke MSRV — cargo-msrv bisects for you:

cargo install cargo-msrv
cargo msrv verify      # builds with the rust-version from Cargo.toml
cargo msrv find        # bisects releases to find the true minimum

Setting the policy is the easier half. The harder half is keeping it: a contributor writes let x = [1, 2, 3].map(|v| v * 2);, which compiles on stable because array::map stabilized in 1.55, or reaches for a newer language feature that stabilized last month — and the MSRV job goes red days later, far from the change that caused it. The hygiene rules that prevent this: set msrv in clippy.toml so Clippy suppresses lints that suggest newer APIs, avoid dependencies that raise MSRV without warning (every transitive dependency's rust-version participates in your effective floor — cargo msrv and cargo tree help find the offender), and decide your bump policy up front. A common one for libraries: MSRV bumps only in minor releases, never in patch, so 1.2.x keeps building forever on 1.75.

State the policy in README.md next to a CI badge, because that is where users look before upgrading. "MSRV is 1.75, bumped only in minor releases, verified in CI" is a one-line promise that turns compiler support from hope into contract. Interviewers probing operational maturity often follow "how do you manage dependencies?" with "what happens when a user's toolchain is older than yours?" — the MSRV field plus a pinned CI job is the answer that shows you have actually shipped a library to strangers, not just compiled it locally.

One related decision closes the loop: which toolchain developers pin locally. rustup default stable gets features but means teammates compile with different compilers, which makes "works on my machine" possible again. A committed rust-toolchain.toml fixes that — every cargo invocation in the repo, including rust-analyzer, picks up the same channel and components:

# rust-toolchain.toml — the team's compiler, not each laptop's
[toolchain]
channel = "1.79.0"
components = ["rustfmt", "clippy"]

The channel here is usually a recent stable for daily work, distinct from the MSRV floor you promise consumers; CI then covers the full matrix — MSRV job proving the floor, stable job running everything. Three compiler versions, each with one job: consumer floor, developer present, release latest.

Shipping Binaries for Other Platforms: Target Triples, cargo audit, cargo-dist

Your CLI runs on your macOS laptop and users want Linux binaries, a Windows .exe, and an Apple Silicon build. Rust solves the cross-compilation half almost trivially: one toolchain compiles for any target whose standard library you have installed, selected by a target triple — x86_64-unknown-linux-gnu, aarch64-pc-windows-msvc, aarch64-apple-darwin — of architecture, vendor, and operating system (plus ABI, where present).

rustup target add x86_64-unknown-linux-gnu
cargo build --release --target x86_64-unknown-linux-gnu
# output lands in target/x86_64-unknown-linux-gnu/release/

The output directory is namespaced per target, so you can build all three from one host without them clobbering each other. Pure-Rust crates cross with no extra setup. Two caveats decide whether your project is among them: builds involving C dependencies (OpenSSL, zstd via *-sys crates) need a linker and the C library for the target — handled practically with cross, which runs each build inside a Docker image that already has the right toolchain — and MSVC Windows targets need the MSVC linker, typically run from a Visual Studio developer prompt or, again, via cross. Windows output also wants .exe in the artifact name when you script the packaging.

Now the release path, where two more gates belong. Before you publish a library — cargo publish, a git tag, whatever your trigger is — check the dependency tree for known security advisories:

cargo install cargo-audit
cargo audit

It reads Cargo.lock, matches every crate against the RustSec advisory database, and exits non-zero on any vulnerability, printing the advisory ID and the patched version range. Run it in CI on a schedule as well as pre-publish: a dependency can be clean when you add it and vulnerable the day someone files the advisory. Libraries usually commit Cargo.lock exclusion-style, but your CI binary and release builds should always audit with a lockfile, because that is the exact tree users receive. cargo deny extends the idea to license policy (deny AGPL in a proprietary product, allow MIT/Apache-2.0/BSD) and duplicate-version detection in one pass.

For distributing the binaries themselves, cargo-dist automates what teams otherwise hand-roll: build matrix, archives, checksums, and a GitHub Release populated on tag. It is worth being precise about why the audit and the distribution belong in the same chapter of your process: the audit answers "should this dependency tree ship?" and cargo-dist answers "how does the artifact reach users?" — a release pipeline that skips either question ships faster until the day it ships something vulnerable to users who cannot easily roll back.

cargo install cargo-dist
cargo dist init        # answers questions, writes config into Cargo.toml

It appends a [package.metadata.dist] section plus a release workflow; cargo dist plan prints exactly which artifacts a tag would produce:

[package.metadata.dist]
targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "x86_64-pc-windows-msvc"]
install-path = "CARGO_HOME"

Then git tag v1.2.0 && git push --tags triggers the workflow: it cross-builds each target, tars/zips them, generates a checksum file, and drafts a release with per-platform install instructions. Homebrew tap formulas and shell completions are opt-in from the same config.

The full baseline, end to end: fmt and clippy gate every push, MSRV compiles every push, audit runs on a schedule, and a tagged release builds, checks, and publishes itself. That is what "production crate" means in practice — the human steps left are reviewing and tagging. When an interviewer asks you to design release engineering for a Rust service, walking through target triples, the audit gate, and tag-triggered builds in that order demonstrates the whole pipeline in ninety seconds. Once the pipeline exists, it is the same machinery the capstones use: CLI Tool Capstone ships a clap-based tool through exactly this release path, and Concurrent Service Capstone benchmarks before and after each tagged optimization.

Quiz

1. Your CI job runs `cargo clippy` and passes even though contributors keep introducing `clone_on_copy` warnings locally. What change makes clippy a real gate?

Question 1 options

2. A library declares `rust-version = "1.75"` in Cargo.toml, but nothing stops a contributor from using a feature that stabilized in 1.80. What catches that before users hit it?

Question 2 options

3. From one macOS laptop you must produce Linux x86_64, Windows x86_64, and Apple Silicon release binaries of a pure-Rust CLI. What is the correct mechanism?

Question 3 options

4. Your binary depends on `openssl-sys 0.9.x`, which later ships an advisory for a CVE. What does `cargo audit` do the next time CI runs it?

Question 4 options

Flashcards

Question

Which four commands belong in a Rust CI gate on every push, and what does each one prevent?

Answer

`cargo fmt --all -- --check` (stops style churn reaching review), `cargo clippy --all-targets --all-features -- -D warnings` (catches whole bug classes pre-merge as build failures), `cargo test --all-features` (unit + integration + doc tests, so compiled examples cannot go stale), and `cargo audit` (fails on known advisories in the locked dependency tree).

Question

What does the `rust-version` field in Cargo.toml actually enforce, and what still needs CI?

Answer

Cargo refuses to build the crate with an older compiler, printing a clear 'requires rustc X or newer' error instead of cryptic syntax failures. It does not test the claim: a CI job pinned to that exact toolchain (usually `cargo check`) must compile the code, and clippy's `msrv` setting must suppress lints that suggest newer APIs.

Question

How does cross-compilation work in Rust, and when does it need extra tooling?

Answer

Target triples (arch-vendor-OS, e.g. x86_64-unknown-linux-gnu) select the platform: `rustup target add <triple>` installs that std and `cargo build --release --target <triple>` emits binaries under target/<triple>/release/. Pure-Rust crates need nothing else; crates with C dependencies need a target linker and libraries (commonly via `cross`), and MSVC Windows targets need the MSVC linker.

Question

What does `cargo audit` check, and when should CI run it?

Answer

It matches every crate in Cargo.lock against the RustSec advisory database and exits non-zero on known vulnerabilities, printing the advisory ID and the patched version range. Run it pre-publish and on a schedule (e.g. weekly), because a clean dependency can gain an advisory after you shipped it — the vulnerability appears between releases, not only at dependency changes.

Question

What does cargo-dist automate for a Rust binary crate?

Answer

It reads its `[package.metadata.dist]` config (targets, install path) and generates a tag-triggered release workflow: cross-builds each target triple, archives and checksums the binaries, and drafts a GitHub Release with per-platform install instructions — replacing hand-written build matrices, packaging scripts, and checksum plumbing with one config section.

Revision Notes

Key Takeaways

  • 1. A merge gate is commands, not discipline: fmt --check, clippy with -D warnings, and test run identically for every contributor because CI runs them for you
  • 2. -D warnings is the switch that makes clippy a gate — warnings never fail a build until you deny them; --all-targets extends coverage to tests, benches, and examples
  • 3. rust-version documents and enforces the compiler floor for consumers, but only a pinned MSRV CI job and clippy's msrv setting keep the claim true as the code evolves
  • 4. One toolchain cross-compiles to any installed target triple; artifacts are namespaced per target, and only C dependencies pull in extra linker machinery
  • 5. cargo audit matches Cargo.lock against RustSec advisories before publishing; cargo-dist turns the whole multi-target build-archive-checksum-release chain into a git tag

Interview Tips

  • • "How do you prevent regressions in a Rust codebase?" — name the four CI commands and the reason for each; ending with doc tests compiling examples shows depth most candidates miss
  • • "What is MSRV and how do you enforce it?" — rust-version in Cargo.toml for the consumer-facing refusal, a pinned 1.75 CI job for the actual proof, and the patch-vs-minor bump policy; that three-part answer signals you have shipped a library
  • • "Walk me through your release process for a multi-platform binary" — target triples with rustup target add, per-target output directories, cargo audit as the publish gate, then cargo-dist on tag; mention the cross/MSVC linker caveat for C dependencies
  • • "A new Rust release broke your CI" — that is what -D warnings looks like in practice; you acknowledge or pin, then update the lints table deliberately, and the same job is what would have caught the MSRV regression

Cheat Sheet

Production Crate Baseline

CI gate (every push):

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo audit            # advisories; also run on a schedule

Lint policy in Cargo.toml (travels with the source):

[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
# clippy.toml: msrv = "1.75", avoid-breaking-exported-api = true

MSRV policy:

[package]
rust-version = "1.75"   # Cargo refuses older compilers
# CI: dtolnay/rust-toolchain@1.75.0 + cargo check --all-features

cargo msrv verify / cargo msrv find — check or bisect the true minimum. Bump MSRV only in minor releases.

Cross-compile + release:

rustup target add x86_64-unknown-linux-gnu
cargo build --release --target x86_64-unknown-linux-gnu
cargo install cargo-audit && cargo audit
cargo dist init && cargo dist plan   # tag-triggered multi-target releases

CI hygiene: dtolnay/rust-toolchain + Swatinem/rust-cache; keep feedback under ~10 minutes or people route around the gate.

Continue Learning