Aller au contenu

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.