Introducción: ¿Por qué necesitas un monorepositorio en Rust?
Cuando empiezas a aprender Rust, lo primero con lo que te encuentras es un proyecto individual creado con cargo new. Pero, ¿qué hacer cuando tu proyecto crece: aparecen librerías compartidas, varios archivos ejecutables (por ejemplo, un servidor y un cliente) o quieres extraer código repetitivo en un paquete separado?
Aquí es donde entra en juego el monorepositorio (monorepo) — un enfoque en el que el código de varios proyectos interrelacionados (librerías y archivos ejecutables) se almacena en un solo repositorio. Rust proporciona una solución integrada para esto: Cargo Workspaces (espacios de trabajo).
En este artículo, analizaremos cómo organizar un monorepositorio en Rust desde cero: desde la estructura de carpetas hasta ejemplos prácticos. Aprenderás cómo dividir correctamente el código en módulos, conectar dependencias locales y gestionar la compilación.
1. ¿Qué es un Cargo Workspace y cómo crearlo?
Un espacio de trabajo (workspace) es un conjunto de crates que utilizan el mismo archivo Cargo.lock y un directorio target compartido. Esto permite evitar conflictos de versiones de dependencias y acelera la compilación.
Creación de la estructura del monorepositorio
Creemos un monorepositorio simple con dos crates: una librería utils y un archivo ejecutable app.
mkdir my_monorepocd my_monorepocargo new utils --libcargo new appAhora creemos el archivo raíz Cargo.toml que declarará el workspace:
[workspace]members = [ "utils", "app",]Importante: Asegúrate de que en los archivos utils/Cargo.toml y app/Cargo.toml no haya una sección [package] con el campo workspace — no es necesaria. Cargo entenderá por sí mismo que son parte del workspace.
Estructura del proyecto
Después de la creación, obtendrás una estructura como esta:
my_monorepo/├── Cargo.toml # workspace raíz├── utils/│ ├── Cargo.toml│ └── src/│ └── lib.rs└── app/ ├── Cargo.toml └── src/ └── main.rs2. Organización del código: librerías y módulos
Ahora agreguemos código a la librería utils. Supongamos que queremos crear funciones comunes para trabajar con números y cadenas.
Código de la librería (utils/src/lib.rs)
pub mod math;pub mod strings;Creemos los archivos de los módulos:
// utils/src/math.rspub fn add(a: i32, b: i32) -> i32 { a + b}
pub fn multiply(a: i32, b: i32) -> i32 { a * b}// utils/src/strings.rspub fn greet(name: &str) -> String { format!("¡Hola, {}!", name)}
pub fn to_uppercase(text: &str) -> String { text.to_uppercase()}Conexión de la librería en el archivo ejecutable
En el archivo app/Cargo.toml indicaremos la dependencia del crate local:
[package]name = "app"version = "0.1.0"edition = "2021"
[dependencies]utils = { path = "../utils" }Ahora en app/src/main.rs podemos usar las funciones de utils:
use utils::math;use utils::strings;
fn main() { let sum = math::add(5, 10); println!("5 + 10 = {}", sum);
let greeting = strings::greet("Mundo"); println!("{}", greeting);
let upper = strings::to_uppercase("rust rocks"); println!("{}", upper);}Ejecuta el proyecto desde la raíz del monorepositorio con el comando:
cargo run -p appVerás la salida:
5 + 10 = 15¡Hola, Mundo!RUST ROCKS3. Organización avanzada: múltiples archivos ejecutables
A menudo en un monorepositorio es necesario tener varios