Introdução
O Cargo é muito mais do que um gerenciador de pacotes — é o sistema de build, gerenciador de dependências, executor de testes e publicador de bibliotecas do ecossistema Rust, tudo em uma ferramenta. Dominar o Cargo é essencial para qualquer projeto Rust que vá além de um simples exercício.
Neste artigo, vamos explorar em profundidade como o Cargo gerencia dependências, desde a sintaxe do Cargo.toml até recursos avançados como workspaces, features condicionais e estratégias de atualização segura.
O Problema: Dependências Desorganizadas
Não Faça Isso: Versões Imprecisas
# ERRADO: Sem Cargo.lock no repositório (para binários)
# Cargo.toml
[dependencies]
serde = "*" # Qualquer versão — pode quebrar a qualquer momento
reqwest = "0" # Aceita 0.x.y, incluindo mudanças incompatíveis
tokio = ">=1.0.0" # Aceita 2.0.0 quando sair, mesmo sendo breaking change
Não Faça Isso: Dependências Desnecessárias
# ERRADO: Puxar crates inteiras para usar uma função
[dependencies]
chrono = "0.4" # Só precisa de timestamp Unix? Use std::time
regex = "1" # Só precisa de split? Use str::split
rand = "0.8" # Só precisa de um ID? Use uuid
num = "0.4" # Só precisa de abs()? Use i32::abs()
Não Faça Isso: Features Ativadas sem Necessidade
# ERRADO: Ativa TODAS as features, incluindo as que não usa
[dependencies]
tokio = { version = "1", features = ["full"] } # Inclui io, fs, net, process, signal...
reqwest = { version = "0.12", features = ["json", "cookies", "gzip", "brotli", "stream"] }
A Solução: Cargo.toml Bem Configurado
Faça Isso: Versionamento Semântico Correto
# Cargo.toml
[dependencies]
# Versão exata (para dependências críticas)
serde = "=1.0.217"
# Patch updates apenas (padrão com ^): aceita 1.0.x
serde = "1.0.217" # Equivalente a "^1.0.217"
# Minor updates: aceita 1.x.y onde x >= 0
serde = "1" # Equivalente a "^1.0.0"
# Tilde: mais restritivo, apenas patches
serde = "~1.0.217" # Aceita >= 1.0.217, < 1.1.0
# Para crates pré-1.0 (0.x), o minor é tratado como major
rand = "0.8" # Aceita >= 0.8.0, < 0.9.0 (NÃO aceita 0.9)
Regra de ouro: Use a forma padrão "1.0" para a maioria das dependências. O operador ^ (implícito) é seguro com SemVer.
Faça Isso: Features Mínimas
# Cargo.toml — ative apenas o necessário
[dependencies]
# Tokio: apenas as features que você usa
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] }
# Serde: derive é quase sempre necessário
serde = { version = "1", features = ["derive"] }
# Reqwest: apenas JSON e TLS
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
# SQLx: runtime e driver específicos
sqlx = { version = "0.8", default-features = false, features = [
"runtime-tokio",
"postgres",
"macros",
] }
Desabilite default-features quando quiser controle total:
# Sem features padrão, apenas o que você precisa
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
Faça Isso: Dependências por Uso
# Cargo.toml
# Dependências principais (usadas em src/)
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
# Dependências apenas para testes
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
tempfile = "3"
mockall = "0.13"
pretty_assertions = "1"
# Dependências apenas para build scripts
[build-dependencies]
cc = "1"
prost-build = "0.13"
# Dependências opcionais (ativadas por features)
[dependencies.tracing]
version = "0.1"
optional = true
Features do Seu Próprio Crate
# Cargo.toml
[features]
default = ["json"]
# Feature que ativa funcionalidade extra
json = ["dep:serde", "dep:serde_json"]
logging = ["dep:tracing"]
full = ["json", "logging"]
[dependencies]
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
tracing = { version = "0.1", optional = true }
// src/lib.rs — código condicional por feature
pub fn processar(dados: &str) -> String {
#[cfg(feature = "logging")]
tracing::info!("Processando {} bytes", dados.len());
dados.to_uppercase()
}
#[cfg(feature = "json")]
pub mod json {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Resposta {
pub mensagem: String,
pub status: u16,
}
pub fn parse(input: &str) -> Result<Resposta, serde_json::Error> {
serde_json::from_str(input)
}
}
Workspaces: Monorepo com Cargo
Para projetos maiores, organize em workspace:
meu-projeto/
├── Cargo.toml # Workspace root
├── crates/
│ ├── core/
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ ├── api/
│ │ ├── Cargo.toml
│ │ └── src/main.rs
│ └── cli/
│ ├── Cargo.toml
│ └── src/main.rs
# Cargo.toml (raiz do workspace)
[workspace]
resolver = "2"
members = [
"crates/core",
"crates/api",
"crates/cli",
]
# Dependências compartilhadas entre membros
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
thiserror = "2"
# Metadados compartilhados
[workspace.package]
version = "0.1.0"
edition = "2024"
authors = ["Equipe Rust Brasil"]
license = "MIT"
# crates/core/Cargo.toml
[package]
name = "meu-projeto-core"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true # Usa a versão do workspace
thiserror.workspace = true
# crates/api/Cargo.toml
[package]
name = "meu-projeto-api"
version.workspace = true
edition.workspace = true
[dependencies]
meu-projeto-core = { path = "../core" } # Dependência local
serde.workspace = true
tokio.workspace = true
Vantagens do workspace:
- Um
Cargo.lockpara todo o projeto — versões consistentes - Compilação compartilhada — dependências comuns são compiladas uma vez
- Dependências centralizadas — atualize a versão em um lugar só
Gerenciamento de Lockfile
Para Bibliotecas: Não commite Cargo.lock
# .gitignore — para bibliotecas
Cargo.lock
Bibliotecas devem ser testadas com as versões mais recentes compatíveis.
Para Aplicações: Sempre commite Cargo.lock
# .gitignore — para aplicações
# NÃO ignore Cargo.lock!
# Isso garante builds reproduzíveis
Atualizando Dependências com Segurança
# Ver dependências desatualizadas
cargo outdated
# Atualizar dentro das restrições do Cargo.toml
cargo update
# Atualizar uma crate específica
cargo update -p serde
# Ver a árvore de dependências
cargo tree
# Encontrar duplicatas
cargo tree --duplicates
# Por que uma dependência está incluída?
cargo tree --invert --package openssl-sys
Guia Passo a Passo: Auditoria de Dependências
Passo 1: Analise a Árvore
# Visualizar toda a árvore
cargo tree
# Output exemplo:
# meu-app v0.1.0
# ├── axum v0.8.1
# │ ├── axum-core v0.5.0
# │ │ ├── http v1.2.0
# │ │ └── ...
# ├── serde v1.0.217
# └── tokio v1.42.0
Passo 2: Identifique Duplicatas
cargo tree --duplicates
# Se houver duas versões de uma crate, considere alinhar
Passo 3: Verifique Vulnerabilidades
cargo install cargo-audit
cargo audit
Passo 4: Verifique Licenças
cargo install cargo-deny
cargo deny check licenses
Passo 5: Atualize com Cuidado
# 1. Crie um branch
# git checkout -b atualizar-deps
# 2. Atualize
cargo update
# 3. Rode todos os testes
cargo test
# 4. Verifique se tudo ainda funciona
cargo clippy
# 5. Commite o Cargo.lock atualizado
Armadilhas Comuns
1. Não Travar Versões em Aplicações
# ERRADO para aplicações: depender de ranges amplos sem Cargo.lock
[dependencies]
serde = "1" # Sem Cargo.lock commitado, cada build pode usar versão diferente
# CORRETO: Commite o Cargo.lock para builds reproduzíveis
# E use `cargo update` periodicamente para atualizar
2. Features Conflitantes
# Cuidado: se duas crates ativam features diferentes de uma mesma dependência,
# o Cargo unifica (union) as features
# Crate A depende de: tokio = { features = ["rt"] }
# Crate B depende de: tokio = { features = ["fs"] }
# Resultado: tokio é compilado com ["rt", "fs"]
# Isso pode causar tempos de compilação maiores que o esperado
3. Usar path em Dependências Publicadas
# ERRADO: path dependencies não funcionam quando publicadas no crates.io
[dependencies]
minha-lib = { path = "../minha-lib" }
# CORRETO: Para publicação, use versão do crates.io
[dependencies]
minha-lib = "1.0"
# Para desenvolvimento local, use [patch]
[patch.crates-io]
minha-lib = { path = "../minha-lib" }
4. Dependência Circular
# ERRADO: Cargo não permite dependências circulares
# crate-a depende de crate-b
# crate-b depende de crate-a → ERRO!
# CORRETO: Extraia a parte comum para um terceiro crate
# crate-common (sem dependências dos outros)
# crate-a depende de crate-common
# crate-b depende de crate-common
5. Ignorar Feature resolver = "2"
# Para workspaces e edition 2021+, sempre use resolver 2
[workspace]
resolver = "2"
# Resolver 2 trata features de dev-dependencies separadamente,
# evitando que features de teste contaminem a compilação normal
Exemplo do Mundo Real: Cargo.toml Completo
[package]
name = "minha-api"
version = "0.1.0"
edition = "2024"
authors = ["Equipe Rust Brasil <contato@rustbrasil.dev>"]
description = "API REST de exemplo com boas práticas de Cargo"
license = "MIT"
repository = "https://github.com/rustlang-br/minha-api"
readme = "README.md"
keywords = ["api", "rest", "rust"]
categories = ["web-programming::http-server"]
[dependencies]
# Web framework
axum = { version = "0.8", features = ["macros"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }
tower = { version = "0.5", features = ["limit", "timeout"] }
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }
# Serialização
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Banco de dados
sqlx = { version = "0.8", features = [
"runtime-tokio",
"postgres",
"macros",
"migrate",
], default-features = false }
# Observabilidade
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
# Erros
thiserror = "2"
anyhow = "1"
# Configuração
dotenvy = "0.15"
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
reqwest = { version = "0.12", features = ["json"] }
tempfile = "3"
tokio = { version = "1", features = ["test-util"] }
[[bench]]
name = "api_benchmarks"
harness = false
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = true
[profile.dev]
opt-level = 0
debug = true
[profile.dev.package."*"]
opt-level = 2 # Otimiza dependências mesmo em debug (compilação mais lenta, mas runtime mais rápido)
Checklist de Gerenciamento de Dependências
- SemVer correto — Use
"1.0"(com^implícito) para a maioria dos casos - Features mínimas — Ative apenas o que você usa
Cargo.lockno git — Para aplicações, sempre commitecargo auditregularmente — Verifique vulnerabilidadescargo outdated— Mantenha dependências atualizadascargo tree— Entenda sua árvore de dependências- Workspaces — Para projetos com múltiplos crates
resolver = "2"— Sempre para edition 2021+dev-dependenciesseparadas — Não misture com dependências de produção- Revise antes de adicionar — Cada dependência é um risco e um custo
Veja Também
- Instalação: Cargo — Primeiros passos com o Cargo
- Erros: Feature Not Found — Como resolver erros de features
- CI/CD para Projetos Rust — Automatize verificação de dependências
- Segurança em Rust — Audite dependências com cargo-audit
- Documentação em Rust — Metadados do Cargo.toml para docs.rs