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.