O Cargo, o sistema de build e gerenciamento de pacotes oficial da linguagem Rust, oferece uma funcionalidade poderosa chamada workspaces (espaços de trabalho), que permite gerenciar múltiplos crates dentro de um único projeto de forma integrada. Essa abordagem é especialmente útil quando se trabalha com aplicações modulares, bibliotecas compartilhadas ou projetos que evoluem em diferentes componentes interdependentes.
Um workspace é um agrupamento de crates que compartilham a mesma configuração de build, dependências e ferramentas. Ele permite que você compile, teste e execute todos os crates do projeto com um único comando, além de garantir que as versões das dependências sejam consistentes entre os módulos.
Workspaces são ideais em cenários como:
Vamos criar um projeto exemplo chamado Sistema de Gerenciamento de Tarefas, composto por três crates:
core
– contém a lógica principal e os modelos de dados.api
– uma aplicação que expõe uma API REST para gerenciar tarefas.cli
– uma interface de linha de comando para interagir com o sistema.Todos os crates pertencem ao mesmo workspace e compartilham a mesma configuração de build.
gerenciador_tarefas/
├── Cargo.toml # Arquivo de workspace raiz
├── core/
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── api/
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
└── cli/
├── Cargo.toml
└── src/
└── main.rs
gerenciador_tarefas/Cargo.toml
[workspace]
members = [
"core",
"api",
"cli"
]
resolver = "2"
Este arquivo define o workspace e lista os crates que fazem parte dele. O campo resolver = "2"
habilita o novo resolvedor de dependências do Cargo, introduzido a partir do Rust 2021. Esse resolvedor melhora significativamente a forma como as dependências são analisadas e unificadas no projeto, especialmente em workspaces. Ele garante que, quando crates diferentes dependem de versões diferentes de uma mesma biblioteca, o Cargo consiga gerenciar essas dependências de forma mais previsível e evitando compilações duplicadas da mesma crate com versões ligeiramente distintas.
Além disso, o resolvedor "2"
permite que crates dentro do mesmo workspace compartilhem dependências de forma mais eficiente, reduzindo o tempo de compilação e o tamanho dos binários finais. Ele também corrige comportamentos indesejados do resolvedor antigo (“1”), como a duplicação de crates no grafo de dependências quando há variações de features ou caminhos de dependência.
Por essas razões, o uso de resolver = "2"
é fortemente recomendado em qualquer projeto com workspace, especialmente quando há dependências compartilhadas entre os crates.
core
– Lógica centralArquivo: gerenciador_tarefas/core/Cargo.toml
[package]
name = "core"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
Arquivo: gerenciador_tarefas/core/src/lib.rs
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Tarefa {
pub id: u32,
pub titulo: String,
pub concluida: bool,
}
pub struct GerenciadorDeTarefas {
tarefas: Vec<Tarefa>,
}
impl GerenciadorDeTarefas {
pub fn novo() -> Self {
GerenciadorDeTarefas {
tarefas: Vec::new(),
}
}
pub fn adicionar_tarefa(&mut self, tarefa: Tarefa) {
self.tarefas.push(tarefa);
}
pub fn listar_tarefas(&self) -> &Vec<Tarefa> {
&self.tarefas
}
}
Este crate define a estrutura de dados e a lógica básica do sistema.
api
– Servidor HTTP simplesArquivo: gerenciador_tarefas/api/Cargo.toml
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.0"
serde = "1.0"
serde_json = "1.0"
core = { path = "../core" }
Arquivo: gerenciador_tarefas/api/src/main.rs
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use core::GerenciadorDeTarefas;
use std::sync::Mutex;
async fn listar_tarefas(
dados: web::Data<Mutex<GerenciadorDeTarefas>>,
) -> impl Responder {
let gerenciador = dados.lock().unwrap();
let tarefas = gerenciador.listar_tarefas();
HttpResponse::Ok().json(tarefas)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let gerenciador = web::Data::new(Mutex::new(GerenciadorDeTarefas::novo()));
println!("Servidor rodando em http://127.0.0.1:8080");
HttpServer::new(move || {
App::new()
.app_data(gerenciador.clone())
.route("/tarefas", web::get().to(listar_tarefas))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Este crate usa Actix-web para criar um servidor que expõe as tarefas. Ele depende do crate core
via caminho local.
cli
– Interface de linha de comandoArquivo: gerenciador_tarefas/cli/Cargo.toml
[package]
name = "cli"
version = "0.1.0"
edition = "2021"
[dependencies]
core = { path = "../core" }
Arquivo: gerenciador_tarefas/cli/src/main.rs
use core::{GerenciadorDeTarefas, Tarefa};
fn main() {
let mut gerenciador = GerenciadorDeTarefas::novo();
let tarefa1 = Tarefa {
id: 1,
titulo: "Estudar Rust".to_string(),
concluida: false,
};
gerenciador.adicionar_tarefa(tarefa1);
println!("Tarefas cadastradas:");
for tarefa in gerenciador.listar_tarefas() {
println!("- [{}] {}", if tarefa.concluida { "x" } else { " " }, tarefa.titulo);
}
}
Este crate demonstra como usar a lógica central em uma aplicação CLI.
cd gerenciador_tarefas
cargo build
cargo run -p cli
cargo run -p api
O Cargo reconhece que todos os crates estão no mesmo workspace e permite executar comandos específicos para cada um com a flag -p
(package).
cargo test
em todos os crates com cargo test --all
.Cargo workspaces são uma ferramenta essencial para projetos Rust que crescem em complexidade. Eles permitem manter a modularidade sem sacrificar a simplicidade de gerenciamento. Com o exemplo apresentado, você pode ver como dividir uma aplicação em componentes coesos, reutilizar código e escalar seu projeto de forma organizada.
Ao adotar workspaces, você está preparado para estruturar projetos profissionais, aplicar boas práticas de arquitetura e facilitar a colaboração em equipes de desenvolvimento.