Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Filename: adder/src/main.rs
fn main() {
    let num = 10;
    println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Listing 14-7: Usando o crate de biblioteca add_one a partir do crate adder

Vamos 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.