rust-workspaces-gestion-crates

Workspaces en Rust: organizar proyectos con crates

  • 4 min

Un workspace de Cargo es una carpeta con varios paquetes y configuración compartida.

Hasta ahora, tus proyectos han seguido el esquema estándar de un solo paquete (crate): un Cargo.toml que define un binario o una librería.

Pero a medida que el software crece, es común querer dividirlo en componentes lógicos independientes:

  • Un Core (la lógica de negocio).
  • Una API (que usa el core).
  • Un CLI (herramienta de línea de comandos que también usa el core).
  • Una Librería de utilidades.

Si hicieras esto con proyectos separados, tendrías que publicar tus librerías en crates.io o usar rutas relativas complicadas, y cada proyecto compilaría sus dependencias por separado (gastando disco y CPU).

Los Workspaces solucionan esto. Permiten tener múltiples paquetes dentro de un mismo proyecto que comparten el mismo directorio target y el mismo archivo Cargo.lock.

Crear un workspace

Un Workspace no es más que una carpeta con un archivo Cargo.toml especial en la raíz. Este archivo no define un paquete, sino que lista a sus “miembros”.

Vamos a crear un proyecto ficticio llamado mi_app que tendrá dos partes:

  1. motor: Una librería con la lógica (calculadora).
  2. cli: Un programa de consola que usa el motor.

Estructura de carpetas

Primero, creamos la carpeta raíz y el archivo de configuración.

mkdir mi_app
cd mi_app
touch Cargo.toml
Copied!

El Cargo.toml del workspace

Abrimos el Cargo.toml raíz y escribimos esto. Fíjate que no tiene sección [package], sino [workspace].

[workspace]
resolver = "3"

members = [
    "motor",
    "cli",
]
Copied!

Crear los miembros

Ahora, dentro de la carpeta mi_app, generamos los dos paquetes usando Cargo normal:

# Crea la librería 'motor'
cargo new motor --lib

# Crea el binario 'cli'
cargo new cli
Copied!

Tu estructura de archivos debería verse así:

mi_app/ ├── Cargo.toml (El del workspace) ├── Cargo.lock (Compartido y único) ├── target/ (Compartido) ├── motor/ │ ├── Cargo.toml │ └── src/lib.rs └── cli/ ├── Cargo.toml └── src/main.rs

Conectando los paquetes

Ahora mismo, motor y cli son vecinos, pero cli no sabe que motor existe. Tenemos que declarar la dependencia.

Abre cli/Cargo.toml y añade la dependencia usando una ruta relativa (path):

[package]
name = "cli"
version = "0.1.0"
edition = "2024"

[dependencies]
# Dependencia local:
motor = { path = "../motor" }
Copied!

Esto le dice a Cargo: “No busques motor en internet, búscalo en la carpeta de al lado”.

Usando la librería

Ahora podemos usar el código de motor dentro de cli.

En motor/src/lib.rs:

pub fn sumar(a: i32, b: i32) -> i32 {
    a + b
}
Copied!

En cli/src/main.rs:

fn main() {
    let resultado = motor::sumar(5, 10);
    println!("El resultado del motor es: {}", resultado);
}
Copied!

Ventajas del workspace

¿Por qué molestarse en hacer esto en lugar de tener carpetas sueltas?

  1. Compilación Compartida: Si tanto motor como cli usan la librería externa serde, el workspace la descargará y compilará una sola vez. Sin workspaces, cada proyecto la compilaría por separado, duplicando el tiempo y el espacio en disco.
  2. Un solo Cargo.lock: Cargo resuelve las dependencias del workspace en conjunto y registra el resultado en un único archivo. Aún puede incluir varias versiones de una misma librería si los requisitos son incompatibles, pero todos los miembros comparten la misma resolución bloqueada.
  3. Tests unificados: Puedes ejecutar cargo test en la raíz y probará todos los paquetes del workspace a la vez.

Comandos en un workspace

Cuando estás en la raíz del workspace, los comandos de Cargo cambian ligeramente su comportamiento:

  • cargo build: Compila todos los miembros del workspace.
  • cargo run: Si Cargo no puede elegir un binario de forma inequívoca, devolverá un error y podremos seleccionar el paquete con -p.
cargo run -p cli
Copied!
  • cargo test -p motor: Ejecuta los tests solo de la librería motor.

Dependencias externas compartidas

En Rust moderno, puedes incluso definir las versiones de las dependencias externas en el Cargo.toml raíz para no tener que repetirlas en cada hijo.

En mi_app/Cargo.toml (Raíz):

[workspace.dependencies]
serde = "1.0"
tokio = "1"
Copied!

En cli/Cargo.toml (Hijo):

[dependencies]
serde = { workspace = true } # Hereda la versión del padre
Copied!

Esto es fantástico para mantener la coherencia en proyectos grandes.