Frontend Labs
⋯

66. Workspaces, CI quality gates, release builds, and cross-compilation

66.1 A workspace for a real service

myservice/
├── Cargo.toml            # [workspace] only
├── Cargo.lock            # ONE lockfile for every crate
├── crates/
│   ├── domain/           # pure types and logic: no I/O, fast to test
│   ├── storage/          # database access; depends on domain
│   ├── api/              # axum handlers; depends on domain + storage
│   └── cli/              # admin tool; reuses domain + storage
└── deny.toml
# Cargo.toml (workspace root)
[workspace]
resolver = "3"
members = ["crates/*"]

[workspace.package]
edition = "2024"
rust-version = "1.85"          # MSRV: the oldest compiler you support; CI should test it

[workspace.dependencies]       # one version of each dependency for the whole repo
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }

[workspace.lints.rust]
unsafe_code = "forbid"         # opt back in per crate, with review, where unsafe is needed

[workspace.lints.clippy]
unwrap_used = "warn"
dbg_macro = "deny"

[profile.release]
lto = "thin"
codegen-units = 1
debug = "line-tables-only"     # useful backtraces and profiles from release builds

Member crates then write tokio.workspace = true and [lints] workspace = true. Splitting into crates is also the main way to cut compile time. Cargo compiles crates in parallel and only recompiles the crates that changed. This mirrors the package boundaries in Go §66. The difference is that Rust enforces the dependency direction, because crates cannot have dependency cycles.

66.2 CI pipeline

# .github/workflows/ci.yml
name: ci
on: [push, pull_request]
jobs:
  check:
    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 --workspace --all-targets --locked -- -D warnings
      - run: cargo test --workspace --locked
      - run: cargo doc --workspace --no-deps --locked
        env: { RUSTDOCFLAGS: "-D warnings" }
  supply-chain:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: EmbarkStudios/cargo-deny-action@v2      # advisories, licenses, bans, sources (§55)
  msrv:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@1.85
      - run: cargo check --workspace --locked
  miri:                                              # only if the workspace contains unsafe code
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@nightly
        with: { components: miri }
      - run: cargo miri test -p storage

Pin third-party actions to a commit SHA rather than a tag in real pipelines. Tags can be moved (Security in Depth Ch 12).

66.3 Cross-compiling

# Fully static Linux binaries (no glibc version problems on old servers):
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl

# Any target without installing a cross C toolchain: zig acts as the linker.
cargo install cargo-zigbuild
cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.17   # also pins the minimum glibc

# Or run the build in a prepared container per target:
cargo install cross && cross build --release --target armv7-unknown-linux-gnueabihf

Static musl binaries are convenient, but musl's default allocator is noticeably slower than glibc's for multi-threaded, allocation-heavy servers. Install mimalloc as the global allocator (§43.2) when you ship musl builds.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36