Introducción
Un monorepositorio (monorepo) es un enfoque para la gestión de código fuente donde múltiples proyectos (bibliotecas, aplicaciones, servicios) se almacenan en un solo repositorio. En el ecosistema de Rust, este enfoque ha recibido un potente soporte a través del mecanismo Cargo Workspace. En este artículo, analizaremos cómo organizar eficazmente un monorepositorio en Rust, examinando la estructura, la gestión de dependencias y las mejores prácticas.
¿Por qué es útil un monorepositorio para proyectos Rust? Permite:
- Reutilizar código entre múltiples crates (bibliotecas y binarios).
- Simplificar el versionado y CI/CD.
- Garantizar la atomicidad de los cambios (un solo commit puede actualizar varios crates).
- Reducir la complejidad de la gestión de dependencias.
Fundamentos de Cargo Workspace
Cargo Workspace es el mecanismo integrado de Rust para gestionar un monorepositorio. Agrupa múltiples crates bajo un único archivo Cargo.toml de nivel superior.
Creación de un workspace
Para empezar, creemos un directorio de proyecto y un archivo Cargo.toml en la raíz:
# Cargo.toml (raíz)[workspace]members = [ "core", "cli", "web-api", "utils"]
[workspace.dependencies]serde = { version = "1.0", features = ["derive"] }tokio = { version = "1.37", features = ["full"] }Aquí hemos definido cuatro crates: core, cli, web-api y utils. El punto clave es [workspace.dependencies]. Esto permite establecer dependencias comunes para todos los crates del workspace. Cada crate puede referenciarlas sin duplicar versiones.
Estructura de directorios
Una estructura típica de un monorepositorio en Rust se ve así:
my-monorepo/├── Cargo.toml # workspace raíz├── Cargo.lock # archivo lock común├── 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.mdCada subdirectorio es un crate independiente con su propio Cargo.toml. Los crates pueden ser tanto bibliotecas (lib.rs) como aplicaciones ejecutables (main.rs).
Gestión de dependencias y reutilización de código
Dependencias internas
Los crates dentro de un workspace pueden referenciarse entre sí mediante rutas locales. Por ejemplo, cli puede depender de core y 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 }Observe la sintaxis { workspace = true } — permite usar las dependencias comunes declaradas en el Cargo.toml raíz. Esto garantiza que todos los crates utilicen la misma versión de la biblioteca.
Ejemplo: biblioteca compartida core
Creemos una biblioteca simple core que contendrá la lógica de negocio:
// 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)}Ahora el crate cli puede usar esta biblioteca:
// cli/src/main.rsuse core::{create_user, greet};
fn main() { let user = create