Conforme un proyecto Rust crece, meterlo todo en un único crate se vuelve incómodo: los tiempos de compilación suben, los tests tardan y la separación de responsabilidades desaparece. Cargo ofrece dos mecanismos para estructurar proyectos grandes: los workspaces, que agrupan varios crates en un único árbol de compilación, y los features, que permiten activar código y dependencias de forma condicional.
Workspace básico
// Estructura de ficheros:
// mi-proyecto/
// ??? Cargo.toml ? raíz del workspace
// ??? crates/
// ? ??? core/
// ? ? ??? Cargo.toml
// ? ? ??? src/lib.rs
// ? ??? api/
// ? ? ??? Cargo.toml
// ? ? ??? src/main.rs
// ? ??? cli/
// ? ??? Cargo.toml
// ? ??? src/main.rs
// mi-proyecto/Cargo.toml (raíz)
[workspace]
members = [
"crates/core",
"crates/api",
"crates/cli",
]
resolver = "2"
# Dependencias compartidas: versión unificada en todo el workspace
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
Crates que comparten dependencias del workspace
// crates/core/Cargo.toml
[package]
name = "core"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { workspace = true } # usa la versión del workspace
// crates/api/Cargo.toml
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]
core = { path = "../core" } # dependencia interna
tokio = { workspace = true }
anyhow = { workspace = true }
// crates/core/src/lib.rs
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct Usuario {
pub id: u32,
pub nombre: String,
}
pub fn version() -> &'static str { "0.1.0" }
Comandos de workspace
// Compilar todo el workspace
// cargo build
// Ejecutar un binario concreto
// cargo run -p cli
// Tests de un crate concreto
// cargo test -p core
// Tests de todo el workspace
// cargo test --workspace
// Un único Cargo.lock para todo: versiones coherentes entre crates
Features: compilación condicional
// crates/core/Cargo.toml
[features]
default = [] # ningún feature activo por defecto
serde = ["dep:serde"] # activar serde solo si lo piden
async = ["dep:tokio"]
full = ["serde", "async"]
[dependencies]
serde = { version = "1", features = ["derive"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
// crates/core/src/lib.rs
pub struct Datos {
pub valor: i32,
}
// Código compilado solo si el feature "serde" está activo
#[cfg(feature = "serde")]
mod serde_impl {
use super::*;
use serde::{Serialize, Deserialize};
impl Serialize for Datos {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut state = s.serialize_struct("Datos", 1)?;
state.serialize_field("valor", &self.valor)?;
state.end()
}
}
}
Features en dependencias opcionales
// Quién consume el crate decide qué features activar:
// Solo lo básico (sin serde ni async)
// [dependencies]
// core = { path = "crates/core" }
// Con soporte de serialización
// [dependencies]
// core = { path = "crates/core", features = ["serde"] }
// Todo incluido
// [dependencies]
// core = { path = "crates/core", features = ["full"] }
// Comprobar si un feature está activo en el código
fn main() {
#[cfg(feature = "async")]
println!("Modo async activo");
#[cfg(not(feature = "async"))]
println!("Modo síncrono");
}
cfg en atributos y código
// Función solo disponible con el feature "debug-logs"
#[cfg(feature = "debug-logs")]
pub fn log_debug(msg: &str) {
eprintln!("[DEBUG] {msg}");
}
// Stub vacío cuando el feature no está activo
#[cfg(not(feature = "debug-logs"))]
pub fn log_debug(_msg: &str) {}
// Plataforma específica
#[cfg(target_os = "linux")]
fn ruta_config() -> &'static str { "/etc/mi-app/config.toml" }
#[cfg(target_os = "macos")]
fn ruta_config() -> &'static str { "~/Library/Application Support/mi-app/config.toml" }
#[cfg(windows)]
fn ruta_config() -> &'static str { "C:\ProgramData\mi-app\config.toml" }
Resumen
- Un workspace agrupa varios crates con un único
Cargo.lock, compilación unificada y tests centralizados. [workspace.dependencies]centraliza versiones para evitar inconsistencias entre crates.- Los crates internos se referencian con
path = "../otro-crate". - Los features permiten activar código y dependencias condicionalmente: quien usa la librería paga solo lo que activa.
optional = trueen una dependencia la convierte en activable por feature.#[cfg(feature = "nombre")]y el bloquecfg()controlan qué código se compila.
