Introduction
Un monorepo (monorepository) est une approche de gestion du code source où plusieurs projets (bibliothèques, applications, services) sont stockés dans un seul dépôt. Dans l'écosystème Rust, cette approche bénéficie d'un support puissant via le mécanisme Cargo Workspace. Dans cet article, nous allons voir comment organiser efficacement un monorepo en Rust, en examinant la structure, la gestion des dépendances et les meilleures pratiques.
Pourquoi un monorepo est-il utile pour les projets Rust ? Il permet de :
- Réutiliser le code entre plusieurs crates (bibliothèques et binaires).
- Simplifier le versioning et le CI/CD.
- Assurer l'atomicité des modifications (un seul commit peut mettre à jour plusieurs crates).
- Réduire la complexité de la gestion des dépendances.
Bases de Cargo Workspace
Cargo Workspace est le mécanisme intégré de Rust pour gérer un monorepo. Il regroupe plusieurs crates sous un seul fichier Cargo.toml de niveau supérieur.
Création d'un workspace
Pour commencer, créons un répertoire de projet et un fichier Cargo.toml à la racine :
# Cargo.toml (racine)[workspace]members = [ "core", "cli", "web-api", "utils"]
[workspace.dependencies]serde = { version = "1.0", features = ["derive"] }tokio = { version = "1.37", features = ["full"] }Ici, nous avons défini quatre crates : core, cli, web-api et utils. Le point clé est [workspace.dependencies]. Cela permet de définir des dépendances communes pour tous les crates du workspace. Chaque crate peut y faire référence sans dupliquer les versions.
Structure des répertoires
Une structure typique de monorepo en Rust ressemble à ceci :
my-monorepo/├── Cargo.toml # workspace racine├── Cargo.lock # fichier de verrouillage commun├── core/│ ├── Cargo.toml│ └── src/│ └── lib.rs├── cli/│ ├── Cargo.toml│ └── src/│ └── main.rs├── web-api/│ ├── Cargo.toml│ └── src/│ └── main.rs├── utils/│ ├── Cargo.toml│ └── src/│ └── lib.rs└── README.mdChaque sous-répertoire est un crate distinct avec son propre Cargo.toml. Les crates peuvent être des bibliothèques (lib.rs) ou des applications exécutables (main.rs).
Gestion des dépendances et réutilisation du code
Dépendances internes
Les crates à l'intérieur d'un workspace peuvent se référer les uns aux autres via des chemins locaux. Par exemple, cli peut dépendre de core et utils :
# cli/Cargo.toml[package]name = "cli"version = "0.1.0"edition = "2021"
[dependencies]core = { path = "../core" }utils = { path = "../utils" }serde = { workspace = true }tokio = { workspace = true }Notez la syntaxe { workspace = true } — elle permet d'utiliser les dépendances communes déclarées dans le Cargo.toml racine. Cela garantit que tous les crates utilisent la même version de la bibliothèque.
Exemple : bibliothèque partagée core
Créons une bibliothèque simple core qui contiendra la logique métier :
// core/src/lib.rsuse serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct User { pub id: u64, pub name: String, pub email: String,}
pub fn create_user(name: String, email: String) -> User { User { id: rand::random(), name, email, }}
pub fn greet(user: &User) -> String { format!("Hello, {}!", user.name)}Maintenant, le crate cli peut utiliser cette bibliothèque :
// cli/src/main.rsuse core::{create_user, greet};
fn main() { let user = create