Workspaces do Cargo
No Capítulo 12, construímos um pacote que incluía um crate binário e um crate de biblioteca. À medida que o projeto evolui, você pode perceber que o crate de biblioteca continua crescendo e desejar dividir ainda mais o pacote em vários crates de biblioteca. O Cargo oferece um recurso chamado workspaces que pode ajudar a gerenciar vários pacotes relacionados, desenvolvidos em conjunto.
Criando um Workspace
Um workspace é um conjunto de pacotes que compartilham o mesmo
Cargo.lock e o mesmo diretório de saída. Vamos criar um projeto usando um
workspace. Usaremos código trivial para que possamos nos concentrar na sua
estrutura. Existem várias maneiras de estruturar um workspace, então
mostraremos apenas uma forma comum. Teremos um workspace contendo um binário e
duas bibliotecas. O binário, que fornecerá a funcionalidade principal,
dependerá das duas bibliotecas. Uma delas fornecerá uma função add_one, e a
outra, uma função add_two. Esses três crates farão parte do mesmo workspace.
Começaremos criando um novo diretório para ele:
$ mkdir add
$ cd add
Em seguida, no diretório add, criamos o arquivo Cargo.toml que configurará
todo o workspace. Esse arquivo não terá uma seção [package]. Em vez disso,
começará com uma seção [workspace], que nos permitirá adicionar membros ao
workspace. Também fazemos questão de usar a versão mais recente do algoritmo de
resolução do Cargo no workspace, definindo o valor de resolver como "3":
Nome do arquivo: Cargo.toml
[workspace]
resolver = "3"
Em seguida, criaremos o crate binário adder executando cargo new dentro do
diretório add:
$ cargo new adder
Created binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
Executar cargo new dentro de um workspace também adiciona automaticamente o
pacote recém-criado à chave members na definição [workspace] do
Cargo.toml do workspace, assim:
[workspace]
resolver = "3"
members = ["adder"]
Neste ponto, podemos compilar o workspace executando cargo build. Os arquivos
do diretório add devem ficar assim:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
O workspace tem um único diretório target no nível superior, no qual os
artefatos compilados serão colocados; o pacote adder não possui seu próprio
diretório target. Mesmo se executássemos cargo build de dentro do
diretório adder, os artefatos compilados ainda terminariam em add/target,
em vez de add/adder/target. O Cargo estrutura o diretório target de um
workspace dessa forma porque os crates em um workspace devem
depender uns dos outros. Se cada crate tivesse seu próprio diretório target,
cada um teria de recompilar os outros crates do workspace para colocar os
artefatos em seu próprio target. Ao compartilhar um único diretório target,
os crates podem evitar recompilações desnecessárias.
Criando o Segundo Pacote no Workspace
Em seguida, vamos criar outro pacote membro no workspace e chamá-lo de
add_one. Gere um novo crate de biblioteca chamado add_one:
$ cargo new add_one --lib
Created library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`
O Cargo.toml de nível superior agora incluirá o caminho add_one na lista
members:
Nome do arquivo: Cargo.toml
[workspace]
resolver = "3"
members = ["adder", "add_one"]
Seu diretório add agora deve ter estes diretórios e arquivos:
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
No arquivo add_one/src/lib.rs, vamos adicionar uma função add_one:
Nome do arquivo: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
Agora podemos fazer o pacote adder, com nosso binário, depender do pacote
add_one, que contém nossa biblioteca. Primeiro, precisaremos adicionar uma
dependência por caminho para add_one em adder/Cargo.toml.
Nome do arquivo: adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
O Cargo não assume que crates em um workspace dependerão uns dos outros, então precisamos ser explícitos sobre essas relações de dependência.
Em seguida, vamos usar a função add_one (do crate add_one) no crate
adder. Abra o arquivo adder/src/main.rs e altere a função main para
chamar add_one, como na Listagem 14-7.
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
add_one a partir do crate adderVamos compilar o workspace executando cargo build no diretório add, no
nível superior!
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
Para executar o crate binário a partir do diretório add, podemos especificar
qual pacote do workspace queremos executar usando o argumento -p e o nome do
pacote com cargo run:
$ cargo run -p adder
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
Isso executa o código em adder/src/main.rs, que depende do add_one crate.
Dependendo de um Pacote Externo
Observe que o workspace possui apenas um arquivo Cargo.lock no nível
superior, em vez de um Cargo.lock no diretório de cada crate. Isso garante
que todos os crates usem a mesma versão de todas as dependências. Se
adicionarmos o pacote rand aos arquivos adder/Cargo.toml e
add_one/Cargo.toml, o Cargo resolverá ambos para uma única versão de rand e
registrará isso no mesmo Cargo.lock. Fazer com que todos os crates do
workspace usem as mesmas dependências significa que eles serão sempre
compatíveis entre si. Vamos adicionar o crate rand à seção [dependencies]
do arquivo add_one/Cargo.toml, para que possamos usá-lo no crate add_one:
Nome do arquivo: add_one/Cargo.toml
[dependencies]
rand = "0.8.5"
Agora podemos adicionar use rand; ao arquivo add_one/src/lib.rs, e compilar
o workspace inteiro executando cargo build no diretório add fará com que o
crate rand seja baixado e compilado. Receberemos um aviso porque não estamos
usando o rand que trouxemos para o escopo:
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
O Cargo.lock de nível superior agora contém informações sobre a dependência de
add_one em rand. No entanto, embora rand seja usado em algum lugar do
workspace, não podemos usá-lo em outros crates do workspace, a menos que o
adicionemos também aos seus arquivos Cargo.toml. Por exemplo, se
adicionarmos use rand; ao arquivo adder/src/main.rs do pacote adder,
obteremos um erro:
$ cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
--> adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
Para corrigir isso, edite o arquivo Cargo.toml do pacote adder e indique
que rand também é uma dependência dele. Compilar o pacote adder
adicionará rand à lista de dependências de adder em Cargo.lock, mas
nenhuma cópia adicional de rand será baixada. O Cargo garantirá que cada
crate de cada pacote do workspace que use o pacote rand utilize a mesma
versão, desde que sejam especificadas versões compatíveis, poupando espaço e
garantindo que os crates do workspace sejam compatíveis entre si.
Se crates do workspace especificarem versões incompatíveis da mesma dependência, o Cargo resolverá cada uma delas, mas ainda tentará resolver o menor número possível de versões.
Adicionando um Teste a um Workspace
Para outra melhoria, vamos adicionar um teste da função add_one::add_one
dentro do crate add_one:
Nome do arquivo: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}
Agora execute cargo test no diretório add, no nível superior. Executar
cargo test em um workspace estruturado dessa forma executará os testes de
todos os crates do workspace:
$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
A primeira seção da saída mostra que o teste it_works no crate add_one
passou. A seção seguinte mostra que nenhum teste foi encontrado no crate
adder, e a última seção mostra que nenhum teste de documentação foi
encontrado no crate add_one.
Também podemos executar testes para um crate específico em um workspace a
partir do diretório de nível superior, usando o sinalizador -p e
especificando o nome do crate que queremos testar:
$ cargo test -p add_one
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Essa saída mostra que cargo test executou apenas os testes do crate
add_one e não executou os testes do crate adder.
Se você publicar os crates do workspace em
crates.io, cada crate precisará ser
publicado separadamente. Assim como em cargo test, podemos publicar um crate
específico do workspace usando o sinalizador -p e especificando o nome do
crate que queremos publicar.
Como prática adicional, adicione um crate add_two a este workspace de forma
semelhante ao crate add_one!
À medida que o projeto cresce, considere usar um workspace: ele permite trabalhar com componentes menores e mais fáceis de entender do que um único grande bloco de código. Além disso, manter os crates em um workspace pode facilitar a coordenação entre eles, especialmente quando costumam ser alterados ao mesmo tempo.