Cargo es mucho más que un gestor de dependencias. Ofrece workspaces para organizar proyectos con múltiples crates, features para compilación condicional, build scripts para generación de código previa a la compilación, y herramientas como cargo-expand para depurar macros. Dominar estas funcionalidades permite escalar proyectos Rust de forma ordenada.
Workspaces: proyectos multicrate con Cargo.lock compartido
Un workspace agrupa varios crates relacionados bajo un único Cargo.toml raíz. Todos comparten el mismo Cargo.lock, lo que garantiza versiones coherentes de dependencias en todo el proyecto. Los comandos de Cargo afectan a todos los crates por defecto, o a uno concreto con -p nombre_crate.
// Estructura de directorios:
// mi-proyecto/
// Cargo.toml (workspace root)
// core/
// Cargo.toml
// src/lib.rs
// api/
// Cargo.toml
// src/main.rs
// cli/
// Cargo.toml
// src/main.rs
# Cargo.toml (raíz del workspace)
[workspace]
members = ["core", "api", "cli"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# core/Cargo.toml
[package]
name = "core"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { workspace = true }
# api/Cargo.toml
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]
core = { path = "../core" }
tokio = { workspace = true }
// Comandos útiles con workspace:
// cargo build -p core -- compilar solo core
// cargo test --workspace -- tests de todos los crates
// cargo clippy --workspace -- linting en todo el workspace
// cargo doc --workspace --open -- documentación de todos los crates
Features condicionales
Las features permiten compilar código opcional. Los usuarios del crate activan las que necesitan; el resto no se compila. Esto reduce el tamaño del binario y las dependencias transitivas.
# Cargo.toml
[features]
default = ["json"]
json = ["serde", "serde_json"]
yaml = ["serde", "serde_yaml"]
full = ["json", "yaml"]
[dependencies]
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
serde_yaml = { version = "0.9", optional = true }
// src/lib.rs
pub fn serializar(datos: &str) -> String {
#[cfg(feature = "json")]
return format!("{{"data":"{datos}"}}");
#[cfg(not(feature = "json"))]
datos.to_string()
}
#[cfg(feature = "yaml")]
pub fn serializar_yaml(clave: &str, valor: &str) -> String {
format!("{clave}: {valor}n")
}
// Activar features desde la línea de comandos:
// cargo build --features yaml
// cargo build --all-features
// cargo build --no-default-features
build.rs: generación de código antes de compilar
build.rs es un script de Rust que Cargo ejecuta antes de compilar el crate principal. Sirve para generar código, compilar bibliotecas C, enlazar librerías nativas o leer variables de entorno de CI.
// build.rs (en la raíz del crate, junto a Cargo.toml)
use std::fs;
use std::env;
use std::path::PathBuf;
fn main() {
// Leer variable de entorno de build y exponerla al código
let version = env::var("BUILD_VERSION").unwrap_or_else(|_| "0.0.0".into());
// Generar un archivo Rust con la versión embebida
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let dest = out_dir.join("version.rs");
fs::write(&dest, format!(
"pub const BUILD_VERSION: &str = "{version}";n"
)).unwrap();
// Decirle a Cargo cuándo re-ejecutar el build script
println!("cargo:rerun-if-env-changed=BUILD_VERSION");
println!("cargo:rerun-if-changed=build.rs");
}
// src/lib.rs incluir el archivo generado
include!(concat!(env!("OUT_DIR"), "/version.rs"));
pub fn version() -> &'static str {
BUILD_VERSION
}
cargo-expand: ver las macros expandidas
cargo-expand muestra el código Rust resultante después de expandir todas las macros del archivo. Es imprescindible para depurar #[derive] personalizados, proc-macros y macros declarativas complejas.
// Instalación:
// cargo install cargo-expand
// Uso básico:
// cargo expand -- expande todo el crate
// cargo expand --bin mi-binario -- expande un binario concreto
// cargo expand mi_modulo -- expande un módulo concreto
// Ejemplo: ver qué genera #[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Punto {
pub x: f64,
pub y: f64,
}
// cargo expand produce algo similar a:
// impl std::fmt::Debug for Punto {
// fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// f.debug_struct("Punto")
// .field("x", &self.x)
// .field("y", &self.y)
// .finish()
// }
// }
// impl Clone for Punto { ... }
// impl PartialEq for Punto { ... }
Un flujo de trabajo habitual en proyectos grandes: define las dependencias compartidas en [workspace.dependencies] para mantener versiones coherentes, usa features para que cada binario solo compile lo que necesita, añade un build.rs para inyectar metadatos de CI (versión, commit hash, fecha de build), y usa cargo-expand cuando alguna macro no produce el código esperado. Juntas, estas herramientas hacen que Cargo escale bien desde proyectos pequeños hasta monorepos con decenas de crates.
