Workspaces: monorepos com Cargo

1. Introdução aos Workspaces no Cargo

Quando um projeto Rust cresce além de um único crate, surge a necessidade de organizar múltiplos crates que se relacionam entre si. É aqui que os workspaces do Cargo entram em cena. Um workspace é um conjunto de um ou mais crates que compartilham o mesmo diretório raiz, o mesmo arquivo Cargo.lock e um processo de build unificado.

A principal diferença entre manter múltiplos crates independentes e usar um workspace está na coordenação. Com crates independentes, cada um possui seu próprio Cargo.lock, suas dependências podem divergir e você precisa gerenciar versões manualmente. Já em um workspace, todos os membros compartilham a resolução de dependências, garantindo consistência.

Os benefícios são claros:
- Dependências compartilhadas: uma única versão de cada dependência é resolvida para todo o workspace
- Build unificado: cargo build compila tudo que precisa ser recompilado de uma só vez
- Versionamento sincronizado: mudanças em múltiplos crates podem ser feitas em um único commit

2. Estrutura e Configuração de um Workspace

Para criar um workspace, você precisa de um Cargo.toml raiz que define a seção [workspace] e lista os membros. A estrutura básica é:

meu-projeto/
├── Cargo.toml          # Workspace raiz
├── Cargo.lock          # Compartilhado
├── crates/
│   ├── lib-core/
│   │   ├── Cargo.toml
│   │   └── src/
│   │       └── lib.rs
│   └── bin-app/
│       ├── Cargo.toml
│       └── src/
│           └── main.rs

O Cargo.toml raiz fica assim:

[workspace]
members = [
    "crates/lib-core",
    "crates/bin-app",
]
resolver = "2"

Você pode usar glob patterns para incluir múltiplos diretórios:

[workspace]
members = ["crates/*"]
exclude = ["crates/legacy-crate"]

A chave exclude é útil para evitar que diretórios específicos sejam tratados como membros do workspace.

3. Gerenciamento de Dependências em Workspaces

Em workspaces, as dependências podem ser declaradas de duas formas: no Cargo.toml raiz (compartilhadas) ou individualmente em cada crate.

Dependências compartilhadas (Rust 2024+):

# Cargo.toml raiz
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# crates/lib-core/Cargo.toml
[dependencies]
serde.workspace = true
tokio.workspace = true

Dependências locais entre crates:

# crates/bin-app/Cargo.toml
[dependencies]
lib-core = { path = "../lib-core" }

O Cargo.lock compartilhado garante que todos os membros usem exatamente as mesmas versões das dependências, evitando o temido "inferno das dependências".

4. Compilação e Build em Workspaces

Os comandos do Cargo funcionam de forma inteligente em workspaces:

# Compila todos os membros
cargo build --workspace

# Compila apenas um crate específico
cargo build -p lib-core

# Verifica se o código compila (mais rápido que build)
cargo check --workspace

# Executa testes de todos os membros
cargo test --workspace

A compilação incremental é compartilhada: se você modificar apenas lib-core, apenas ele e seus dependentes diretos serão recompilados. O cache de compilação fica em target/ na raiz do workspace.

Flags úteis:
- --workspace: opera em todos os membros
- --package <nome> ou -p <nome>: opera em um membro específico
- --all: sinônimo de --workspace (depreciado em versões recentes)

5. Organização e Versionamento de Crates

Workspaces oferecem flexibilidade no versionamento. Você pode optar por:

Versionamento independente: cada crate tem sua própria versão no Cargo.toml.

Versionamento sincronizado com workspace.package:

# Cargo.toml raiz
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Seu Nome"]
license = "MIT"
# crates/lib-core/Cargo.toml
[package]
name = "lib-core"
version.workspace = true
edition.workspace = true

Para publicar no crates.io seletivamente:

cargo publish -p lib-core
cargo publish -p bin-app

Boas práticas incluem manter CHANGELOG.md por crate e usar tags Git como lib-core-v0.2.0 para rastrear versões.

6. Testes e Integração Contínua em Monorepos

Executar testes em todo o workspace é simples:

cargo test --workspace

Para CI com GitHub Actions, um exemplo básico:

name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions-rust-lang/setup-rust-toolchain@v1
      - run: cargo test --workspace

Para isolar falhas e testar apenas crates modificados, ferramentas como cargo-watch e scripts personalizados com git diff podem ser usados:

# Exemplo conceitual (requer script customizado)
cargo test -p $(git diff --name-only HEAD~1 | grep '^crates/' | cut -d'/' -f2 | sort -u)

7. Ferramentas e Dicas Avançadas

Workspaces aninhados: você pode ter workspaces dentro de workspaces, embora isso seja raro e geralmente desnecessário.

[workspace.dependencies]: centraliza todas as dependências, facilitando atualizações em massa.

Integração com ferramentas:
- cargo-make: task runner para automatizar builds complexos
- cargo-release: gerencia versionamento e publicação de múltiplos crates
- cargo-profiler: para análise de desempenho em monorepos

Exemplo de uso com cargo-make:

# Makefile.toml
[tasks.build-all]
command = "cargo"
args = ["build", "--workspace"]

[tasks.test-all]
command = "cargo"
args = ["test", "--workspace"]

8. Considerações Finais e Casos de Uso

Workspaces não são para todos os projetos. Evite usá-los quando:
- O projeto é muito pequeno (um ou dois crates simples)
- As equipes são desconexas e preferem releases independentes
- Há necessidade de versionamento drasticamente diferente entre crates

Exemplos reais de uso bem-sucedido:
- Servo: o motor de renderização da Mozilla usa workspaces extensivamente
- Tokio: a runtime assíncrona tem múltiplos crates em um workspace
- Rust Analyzer: o LSP para Rust é um monorepo complexo com dezenas de crates

Para se aprofundar, explore temas como features condicionais em workspaces, documentação unificada com cargo doc --workspace e estratégias de publicação contínua.


Referências