Gerenciamento de Dependências Rust: Cargo | Rust Brasil

Guia completo de gerenciamento de dependências em Rust com Cargo: Cargo.toml, features, workspaces, versioning e crates.io.

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.lock para 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

  1. SemVer correto — Use "1.0" (com ^ implícito) para a maioria dos casos
  2. Features mínimas — Ative apenas o que você usa
  3. Cargo.lock no git — Para aplicações, sempre commite
  4. cargo audit regularmente — Verifique vulnerabilidades
  5. cargo outdated — Mantenha dependências atualizadas
  6. cargo tree — Entenda sua árvore de dependências
  7. Workspaces — Para projetos com múltiplos crates
  8. resolver = "2" — Sempre para edition 2021+
  9. dev-dependencies separadas — Não misture com dependências de produção
  10. Revise antes de adicionar — Cada dependência é um risco e um custo

Veja Também