Construction et packaging Rust¶
Cargo est l'outil central de l'écosystème Rust : il géré les dépendances, la compilation, les tests, la publication et les workspaces multi-crates. La combinaison Cargo + cross permet de compiler des binaires statiques pour n'importe quelle cible sans avoir la toolchain cible installee localement.
Cargo — structure d'un projet¶
# Cargo.toml — manifeste complet d'un projet Rust
[package]
name = "mon-service"
version = "1.2.0"
edition = "2024"
description = "Service REST exemple"
license = "MIT"
authors = ["Prenom Nom <email@example.com>"]
repository = "https://github.com/user/mon-service"
keywords = ["web", "api", "rest"]
categories = ["web-programming"]
# Binaire principal
[[bin]]
name = "mon-service"
path = "src/main.rs"
# Bibliotheque optionnelle (si le crate expose aussi une API)
[lib]
name = "mon_service_lib"
path = "src/lib.rs"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
tracing = "0.1"
# Dependances uniquement en dev et test
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
insta = "1"
tower = { version = "0.4", features = ["util"] }
# Dependances uniquement en build script
[build-dependencies]
vergen = { version = "8", features = ["git", "build"] }
# Profil release — optimisations maximales
[profile.release]
opt-level = 3
lto = "thin" # Link-time optimization legere
codegen-units = 1 # Un seul codegen unit : meilleure LTO
strip = "symbols" # Supprime les symboles de debug
# Profil dev — compilation rapide avec debug info
[profile.dev]
opt-level = 0
debug = true
Cargo.lock et reproductibilite¶
Cargo.lock fixe les versions exactes de toutes les dépendances transitives. Il doit être commite pour les applications (binaires), mais ignore pour les bibliotheques.
# Afficher le graphe de dependances
cargo tree
# Verifier les mises a jour disponibles
cargo outdated # necessite cargo-outdated
# Mettre a jour une dependance specifique
cargo update -p serde
# Mettre a jour toutes les dependances dans les contraintes SemVer
cargo update
Workspaces — projets multi-crates¶
Un workspace groupe plusieurs crates avec un Cargo.lock partage et une seule cible de compilation.
monorepo/
├── Cargo.toml ← workspace root
├── Cargo.lock ← partage entre tous les membres
├── api/
│ └── Cargo.toml ← membre du workspace
├── core/
│ └── Cargo.toml ← membre du workspace
└── cli/
└── Cargo.toml ← membre du workspace
# Cargo.toml (workspace root)
[workspace]
members = ["api", "core", "cli"]
resolver = "2" # Resolver de features recommande depuis Rust 2021
# Dependances partagees — evite les doublons de versions
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
# api/Cargo.toml — utilise les dependances du workspace
[package]
name = "api"
version = "0.1.0"
edition = "2024"
[dependencies]
core = { path = "../core" }
serde = { workspace = true } # Version du workspace
tokio = { workspace = true }
# Compiler tous les membres du workspace
cargo build --workspace
# Tester un membre specifique
cargo test -p api
# Lancer le binaire d'un membre
cargo run -p cli
Features et compilation conditionnelle¶
Les features permettent de compiler des fonctionnalités optionnelles, reduisant la taille des binaires et les dépendances.
# Cargo.toml — declaration des features
[features]
default = ["json"] # Features actives par defaut
json = ["dep:serde_json"]
postgres = ["dep:sqlx/postgres"]
metrics = ["dep:prometheus"]
full = ["json", "postgres", "metrics"]
[dependencies]
serde_json = { version = "1", optional = true }
sqlx = { version = "0.8", optional = true }
prometheus = { version = "0.13", optional = true }
// Compilation conditionnelle avec #[cfg(feature = "...")]
#[cfg(feature = "json")]
pub mod json_utils {
use serde_json::Value;
pub fn parser(s: &str) -> Result<Value, serde_json::Error> {
serde_json::from_str(s)
}
}
#[cfg(feature = "postgres")]
pub async fn connecter_postgres(url: &str) -> sqlx::PgPool {
sqlx::PgPool::connect(url).await.expect("Connexion Postgres echouee")
}
// Activer des features depuis la ligne de commande
// cargo build --features "postgres,metrics"
// cargo build --all-features
// cargo build --no-default-features --features json
Cross-compilation avec cross¶
cross est un wrapper autour de cargo qui utilise Docker pour cross-compiler sans installer les toolchains cibles manuellement.
# Installation
cargo install cross
# Cibles courantes
# Linux x86_64 (musl — binaire 100% statique)
cross build --release --target x86_64-unknown-linux-musl
# Linux ARM64 (pour Raspberry Pi, serveurs ARM)
cross build --release --target aarch64-unknown-linux-gnu
# Windows depuis Linux
cross build --release --target x86_64-pc-windows-gnu
# macOS depuis Linux (necessite osxcross — non supporte par cross)
# Preferer la compilation native sur macOS ou GitHub Actions macOS runner
# Lister les cibles supportees par Rust
rustup target list
musl vs glibc
La cible x86_64-unknown-linux-musl produit un binaire 100% statique : aucune dépendance vers glibc, NSS ou ld.so. Ce binaire tourne sur n'importe quelle distribution Linux, y compris Alpine. C'est la cible idéale pour les images Docker minimalistes.
Dockerfile multi-stage Rust¶
# Dockerfile — build multi-stage avec image finale distroless
# Stage 1 : compilation
FROM rust:1.85-slim AS builder
WORKDIR /app
# Cache des dependances — exploite le cache Docker si Cargo.toml n'a pas change
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm src/main.rs
# Compilation de l'application reelle
COPY src ./src
COPY migrations ./migrations
RUN touch src/main.rs && cargo build --release
# Stage 2 : image finale minimale (distroless)
# gcr.io/distroless/cc contient uniquement glibc + certificats TLS
FROM gcr.io/distroless/cc-debian12
WORKDIR /app
COPY --from=builder /app/target/release/mon-service /app/mon-service
COPY --from=builder /app/migrations /app/migrations
# Utilisateur non-root pour la securite
USER nonroot:nonroot
EXPOSE 3000
ENTRYPOINT ["/app/mon-service"]
# Construction et test de l'image
docker build -t mon-service:latest .
docker run --rm -p 3000:3000 mon-service:latest
# Verifier la taille de l'image
docker images mon-service
# Typiquement 15-30 Mo pour un service Axum + distroless
Publication sur crates.io¶
# Connexion a crates.io (token depuis https://crates.io/settings/tokens)
cargo login <token>
# Verifier le package avant publication
cargo package --list # Fichiers inclus
cargo publish --dry-run # Simulation sans envoi
# Publication
cargo publish
# Publication d'un workspace member
cargo publish -p nom-du-crate
# Cargo.toml — champs requis pour la publication
[package]
name = "mon-crate"
version = "0.1.0"
edition = "2024"
description = "Description courte (140 chars max)"
license = "MIT OR Apache-2.0" # Double licence standard Rust
repository = "https://github.com/user/mon-crate"
readme = "README.md"
# Exclure les fichiers inutiles du package
exclude = [
"tests/fixtures/**",
".github/**",
"benches/**",
]
cargo-binstall — installation de binaires sans compilation¶
cargo-binstall installe les binaires Rust depuis GitHub Releases au lieu de les compiler depuis les sources, reduisant le temps d'installation de plusieurs minutes a quelques secondes.
# Installation de cargo-binstall
cargo install cargo-binstall
# Installer un outil via les releases binaires
cargo binstall ripgrep
cargo binstall cargo-llvm-cov
cargo binstall cargo-deny
# Equivalent sans compilation locale
# cargo install ripgrep ← compile depuis les sources (~2 minutes)
# cargo binstall ripgrep ← telecharge le binaire (~5 secondes)
Pipeline CI/CD — GitHub Actions¶
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings" # Les warnings Clippy deviennent des erreurs
jobs:
test:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Installer Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
- name: Verifier le formatage
run: cargo fmt --check
- name: Clippy (linter)
run: cargo clippy --all-targets --all-features
- name: Tests
run: cargo test --all-features
- name: Couverture (llvm-cov)
run: |
cargo install cargo-llvm-cov
cargo llvm-cov --lcov --output-path lcov.info
- name: Upload couverture vers Codecov
uses: codecov/codecov-action@v4
with:
files: lcov.info
release:
name: Release binaires
if: startsWith(github.ref, 'refs/tags/')
needs: test
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Compiler
run: cargo build --release --target ${{ matrix.target }}
- name: Uploader l'artefact
uses: actions/upload-artifact@v4
with:
name: binaire-${{ matrix.target }}
path: target/${{ matrix.target }}/release/mon-service*
Cache Cargo en CI
Sans cache, chaque run CI recompile toutes les dépendances depuis zero. Swatinem/rust-cache met en cache ~/.cargo/registry et target/. Sur un projet avec beaucoup de dépendances (Axum, SQLx, Tokio), le gain peut être de 5 a 10 minutes par run.