Cargo Workspaces

En el Capítulo 12, construimos un paquete que incluía un crate binario y un crate de biblioteca. A medida que su proyecto se desarrolle, es posible que encuentre que el crate de biblioteca continúa creciendo y que desea dividir su paquete aún más en múltiples crate de biblioteca. Cargo ofrece una característica llamada workspaces que puede ayudar a administrar múltiples paquetes relacionados que se desarrollan en tándem.

Creando un Workspace

Un workspace es un conjunto de paquetes que comparten el mismo Cargo.lock y el directorio de salida. Hagamos un proyecto usando un workspace - usaremos código trivial para que podamos concentrarnos en la estructura del workspace. Hay varias formas de estructurar un workspace, así que solo mostraremos una forma común. Tendremos un workspace que contiene un binario y dos bibliotecas. El binario, que proporcionará la funcionalidad principal, dependerá de las dos bibliotecas. Una biblioteca proporcionará una función add_one, y una segunda biblioteca una función add_two. Estas tres cajas serán parte del mismo workspace. Comenzaremos creando un nuevo directorio para el workspace:

$ mkdir add
$ cd add

Luego, en el directorio add, crearemos el archivo Cargo.toml que configurará todo el workspace. Este archivo no tendrá una sección [package]. En su lugar, comenzará con una sección [workspace] que nos permitirá agregar miembros al workspace especificando la ruta al paquete con nuestro crate binario; en este caso, esa ruta es adder:

Filename: Cargo.toml

[workspace]

members = [
    "adder",
]

A continuación, crearemos el crate binario adder ejecutando cargo new dentro del directorio add:

$ cargo new adder
     Created binary (application) `adder` package

En este punto, podemos construir el workspace ejecutando cargo build. Los archivos en su directorio add deberían verse así:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

El workspace tiene un directorio target en el nivel superior que contendrá los artefactos compilados. El paquete adder no tiene su propio directorio target. Incluso si ejecutáramos cargo build desde dentro del directorio adder, los artefactos compilados aún terminarían en add/target en lugar de add/adder/target. Cargo estructura el directorio target en un workspace de esta manera porque los crate en un workspace están destinados a dependerse entre sí. Si cada crate tuviera su propio directorio target, cada crate tendría que volver a compilar cada uno de los otros crate en el workspace para colocar los artefactos en su propio directorio target. Al compartir un directorio target, los crate pueden evitar la reconstrucción innecesaria.

Creando el Segundo Paquete en el Workspace

A continuación crearemos otro paquete miembro en el workspace y lo llamaremos add_one. Cambie el Cargo.toml de nivel superior para especificar la ruta add_one en la lista de members:

Filename: Cargo.toml

[workspace]

members = [
    "adder",
    "add_one",
]

Luego generaremos un nuevo crate de biblioteca llamado add_one:

$ cargo new add_one --lib
     Created library `add_one` package

Tu directorio add debería tener estos directorios y archivos:

├── Cargo.lock
├── Cargo.toml
├── add_one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

En el archivo add_one/lib.rs, agreguemos una función add_one:

Filename: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

Ahora podemos hacer que el paquete adder con nuestro binario dependa del paquete add_one con nuestra biblioteca. Primero, necesitaremos agregar una dependencia de ruta en adder/Cargo.toml.

Filename: adder/Cargo.toml

[dependencies]
add_one = { path = "../add_one" }

Cargo no asume que los crates en un workspace dependerán entre sí, por lo que necesitamos ser explícitos sobre las relaciones de dependencia.

A continuación, usaremos la función add_one (del crate add_one) en el crate adder. Abra el archivo adder/src/main.rs y agregue una línea use en la parte superior para traer el nuevo crate de biblioteca add_one al alcance. Luego cambie la función main para llamar a la función add_one, como en el Listado 14-7.

Filename: adder/src/main.rs

use add_one;

fn main() {
    let num = 10;
    println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}

Listing 14-7: Usando el crate de biblioteca add_one desde el crate adder

¡Construyamos el workspace ejecutando cargo build en el directorio superior add!

$ cargo build
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.68s

Para ejecutar el crate binario desde el directorio add, podemos especificar qué paquete en el workspace queremos ejecutar con el argumento -p y el nombre del paquete con cargo run:

$ cargo run -p adder
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

Esto ejecuta el código en adder/src/main.rs, que depende del crate add_one.

Dependiendo de un Paquete Externo en un Workspace

Observa que el workspace tiene solo un archivo Cargo.lock en el nivel superior, en lugar de tener un Cargo.lock en cada directorio de crate. Esto asegura que todos los crate estén usando la misma versión de todas las dependencias. Si agregamos el paquete rand al Cargo.toml de adder y add_one, Cargo resolverá ambos a una versión de rand y lo registrará en el único Cargo.lock. Hacer que todos los crate en el workspace usen las mismas dependencias significa que los crate siempre serán compatibles entre sí. Agreguemos el crate rand a la sección [dependencies] en el archivo add_one/Cargo.toml para que podamos usar el crate rand en el crate add_one:

Filename: add_one/Cargo.toml

[dependencies]
rand = "0.8.5"

Ahora podemos agregar use rand; al archivo add_one/src/lib.rs, y construir todo el workspace ejecutando cargo build en el directorio add traerá e compilará el crate rand. Obtendremos una advertencia porque no nos estamos refiriendo al rand que trajimos al scope:

$ 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
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 10.18s

El archivo Cargo.lock de nivel superior ahora contiene información sobre la dependencia de add_one en rand. Sin embargo, aunque rand se usa en algún lugar del workspace, no podemos usarlo en otros crate del workspace a menos que agreguemos rand a sus archivos Cargo.toml también. Por ejemplo, si agregamos use rand; al archivo adder/src/main.rs para el paquete adder, obtendremos un error:

$ 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 solucionar esto, edita el archivo Cargo.toml del paquete adder e indica que rand es una dependencia para él también. Construir el paquete adder agregará rand a la lista de dependencias para adder en Cargo.lock, pero no se descargarán copias adicionales de rand. Cargo se asegurara de que cada crate en cada paquete en el workspace que usa el paquete rand estará usando la misma versión siempre y cuando se especifiquen como versiones compatibles de rand, ahorrándonos espacio y asegurando que los crate en el workspace serán compatibles entre sí.

Agregando un Test a un Workspace

Para otra mejora, agreguemos una prueba de la función add_one::add_one dentro del crate add_one:

Filename: 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));
    }
}

Ahora ejecutamos cargo test en el directorio superior add para ejecutar los tests una estructura de workspace como esta ejecutará los tests para todos los crates en el 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 [unoptimized + debuginfo] target(s) in 0.27s
     Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841)

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-49979ff40686fa8e)

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

La primera sección del output muestra que el test it_works en el crate add_one pasó. La siguiente sección muestra que no se encontraron tests en el crate adder, y luego la última sección muestra que no se encontraron tests de documentación en el crate add_one.

También podemos ejecutar tests para un crate en particular en el workspace desde el directorio superior usando la bandera -p y especificando el nombre del crate que queremos testear:

$ cargo test -p add_one
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/lib.rs (target/debug/deps/add_one-b3235fea9a156f74)

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

Este output muestra que cargo test solo ejecutó los tests para el crate add_one y no ejecutó los tests del crate adder.

Si tu publicas los crates en el workspace en crates.io, cada crate en el workspace necesitará ser publicado por separado. Como cargo test, podemos publicar un crate en particular en nuestro workspace usando la bandera -p y especificando el nombre del crate que queremos publicar.

Para practicar aún más, agrega un crate add_two a este workspace de manera similar al crate add_one!

Conforme tu proyecto crece, considera usar un workspace: es más fácil de entender componentes pequeños e individuales que un gran blob de código. Además, mantener los crates en un workspace puede hacer que la coordinación entre crates sea más fácil si se cambian a menudo al mismo tiempo.