Les espaces de travail de cargo

Dans le chapitre 12, nous avons construit un paquet qui comprenait une crate binaire et une crate de bibliothèque. Au fur et à mesure que votre projet se développe, vous pourrez constater que la crate de bibliothèque continue de s'agrandir et vous voudriez alors peut-être diviser votre paquet en plusieurs crates de bibliothèque. Pour cette situation, cargo a une fonctionnalité qui s'appelle les espaces de travail qui peuvent aider à gérer plusieurs paquets liés qui sont développés en tandem.

Créer un espace de travail

Un espace de travail est un jeu de paquets qui partagent tous le même Cargo.lock et le même dossier de sortie. Créons donc un projet en utilisant un espace de travail — nous allons utiliser du code trivial afin de nous concentrer sur la structure de l'espace de travail. Il existe plusieurs façons de structurer un espace de travail ; nous allons vous montrer une manière commune d'organisation. Nous allons avoir un espace de travail contenant un binaire et deux bibliothèques. Le binaire, qui devrait fournir les fonctionnalités principales, va dépendre des deux bibliothèques. Une bibliothèque va fournir une fonction ajouter_un, et la seconde bibliothèque, une fonction ajouter_deux. Ces trois crates feront partie du même espace de travail. Nous allons commencer par créer un nouveau dossier pour cet espace de travail :

$ mkdir ajout
$ cd ajout

Ensuite, dans le dossier ajout, nous créons le fichier Cargo.toml qui va configurer l'intégralité de l'espace de travail. Ce fichier n'aura pas de section [package] ou les métadonnées que nous avons vues dans les autres fichiers Cargo.toml. A la place, il commencera par une section [workspace] qui va nous permettre d'ajouter des membres à l'espace de travail en renseignant le chemin vers le paquet qui contient notre crate binaire ; dans ce cas, ce chemin est additioneur :

Fichier : Cargo.toml

[workspace]

members = [
    "additioneur",
]

Ensuite, nous allons créer la crate binaire additioneur en lançant cargo new dans le dossier ajout :

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

A partir de ce moment, nous pouvons compiler l'espace de travail en lançant cargo build. Les fichiers dans votre dossier ajout devraient ressembler à ceci :

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

L'espace de travail a un dossier target au niveau le plus haut pour y placer les artefacts compilés ; le paquet additioneur n'a pas son propre dossier target. Même si nous lancions cargo build à l'intérieur du dossier additioneur, les artefacts compilés finirons toujours dans ajout/target plutôt que dans ajout/additioneur/target. Cargo organise ainsi le dossier target car les crates d'un espace de travail sont censés dépendre l'une de l'autre. Si chaque crate avait son propre dossier target, chaque crate devrait recompiler chacune des autres crates présentes dans l'espace de travail pour avoir les artefacts dans son propre dossier target. En partageant un seul dossier target, les crates peuvent éviter des re-compilations inutiles.

Créer le second paquet dans l'espace de travail

Ensuite, créons un autre paquet, membre de l'espace de travail et appelons-le ajouter_un. Changeons le Cargo.toml du niveau le plus haut pour renseigner le chemin vers ajouter_un dans la liste members :

Fichier : Cargo.toml

[workspace]

members = [
    "additioneur",
    "ajouter_un",
]

Ensuite, générons une nouvelle crate de bibliothèque ajouter_un :

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

Votre dossier ajout devrait maintenant avoir ces dossiers et fichiers :

├── Cargo.lock
├── Cargo.toml
├── ajouter_un
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── additioneur
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

Dans le fichier ajouter_un/src/lib.rs, ajoutons une fonction ajouter_un :

Fichier : ajouter_un/src/lib.rs

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

Maintenant que nous avons un autre paquet dans l'espace de travail, nous pouvons faire en sorte que le paquet additioneur qui contient notre binaire dépende du paquet ajouter_un, qui contient notre bibliothèque. D'abord, nous devons ajouter un chemin de dépendance à ajouter_un dans additioneur/Cargo.toml.

Fichier : additioneur/Cargo.toml

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

Cargo ne fait pas la supposition que les crates d'un espace de travail dépendent l'une de l'autre, donc vous devez être explicites sur les relations de dépendance entre les crates.

Ensuite, utilisons la fonction ajouter_un de la crate ajouter_un dans la crate additioneur. Ouvrez le fichier additioneur/src/main.rs et ajoutez une ligne use tout en haut pour importer la bibliothèque ajouter_un dans la portée. Changez ensuite la fonction main pour appeler la fonction ajouter_un, comme dans l'encart 14-7.

Fichier : additioneur/src/main.rs

use ajouter_un;

fn main() {
    let nombre = 10;
    println!(
        "Hello, world ! {} plus un vaut {} !",
        nombre,
        ajouter_un::ajouter_un(nombre)
    );
}

Encart 14-7 : utilisation de la bibliothèque ajouter_un dans la crate additioneur

Compilons l'espace de travail en lançant cargo build dans le niveau le plus haut du dossier ajout !

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

Pour lancer la crate binaire à partir du dossier ajout, nous pouvons préciser quel paquet nous souhaitons exécuter dans l'espace de travail en utilisant l'argument -p suivi du nom du paquet avec cargo run :

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

Cela exécute le code de additioneur/src/main.rs, qui dépend de la crate ajouter_un.

Dépendre d'un paquet externe dans un espace de travail

Notez que l'espace de travail a un seul fichier Cargo.lock dans le niveau le plus haut de l'espace de travail plutôt que d'avoir un Cargo.lock dans chaque dossier de chaque crate. Cela garantit que toutes les crates utilisent la même version de toutes les dépendances. Si nous ajoutons le paquet rand aux fichiers additioneur/Cargo.toml et ajouter_un/Cargo.toml, cargo va réunir les deux en une seule version de rand et enregistrer cela dans un seul Cargo.lock. Faire en sorte que toutes les crates de l'espace de travail utilisent la même dépendance signifie que les crates dans l'espace de travail seront toujours compatibles l'une avec l'autre. Ajoutons la crate rand à la section [dependencies] du fichier ajouter_un/Cargo.toml pour pouvoir utiliser la crate rand dans la crate ajouter_un :

Fichier : ajouter_un/Cargo.toml

[dependencies]
rand = "0.8.3"

Nous pouvons maintenant ajouter use rand; au fichier ajouter_un/src/lib.rs et compiler l'ensemble de l'espace de travail en lançant cargo build dans le dossier ajout, ce qui va importer et compiler la crate rand. Nous devrions avoir un avertissement car nous n'avons pas utilisé le rand que nous avons introduit dans la portée :

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.3
   -- partie masquée ici --
   Compiling rand v0.8.3
   Compiling ajouter_un v0.1.0 (file:///projects/ajout/ajouter_un)
warning: unused import: `rand`
 --> ajouter_un/src/lib.rs:1:5
  |
1 | use rand;
  |     ^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: 1 warning emitted

   Compiling additioneur v0.1.0 (file:///projects/ajout/additioneur)
    Finished dev [unoptimized + debuginfo] target(s) in 10.18s

Le Cargo.lock du niveau le plus haut contient maintenant les informations de dépendance à rand pour ajouter_un. Cependant, même si rand est utilisé quelque part dans l'espace de travail, nous ne pouvons pas l'utiliser dans d'autres crates de l'espace de travail tant que nous n'ajoutons pas rand dans leurs fichiers Cargo.toml. Par exemple, si nous ajoutons use rand; dans le fichier additioneur/src/main.rs pour le paquet additioneur, nous allons avoir une erreur :

$ cargo build
  -- partie masquée ici --
   Compiling additioneur v0.1.0 (file:///projects/ajout/additioneur)
error[E0432]: unresolved import `rand`
 --> additioneur/src/main.rs:2:5
  |
2 | use rand;
  |     ^^^^ no external crate `rand`

Pour corriger cela, modifiez le fichier Cargo.toml pour le paquet additioneur et indiquez que rand est une dépendance de cette crate aussi. La compilation du paquet additioneur va rajouter rand à la liste des dépendances pour additioneur dans Cargo.lock, mais aucune copie supplémentaire de rand ne sera téléchargée. Cargo s'est assuré que toutes les crates de chaque paquet de l'espace de travail qui utilise le paquet rand seraient de la même version. Utiliser la même version de rand dans les espaces de travail économise de l'espace car nous n'avons pas à multiplier les copies, ni à nous assurer que les crates dans l'espace de travail sont compatibles les unes avec les autres.

Ajouter un test à l'espace de travail

Afin de procéder à une autre amélioration, ajoutons un test de la fonction ajouter_un::ajouter_un dans la crate ajouter_un :

Fichier : add_one/src/lib.rs

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cela_fonctionne() {
        assert_eq!(3, ajouter_un(2));
    }
}

Lancez maintenant cargo test dans le niveau le plus haut du dossier ajout :

$ cargo test
   Compiling ajouter_un v0.1.0 (file:///projects/ajout/ajouter_un)
   Compiling additioneur v0.1.0 (file:///projects/ajout/additioneur)
    Finished test [unoptimized + debuginfo] target(s) in 0.27s
     Running target/debug/deps/ajouter_un-f0253159197f7841

running 1 test
test tests::cela_fonctionne ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running target/debug/deps/additioneur-49979ff40686fa8e

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests ajouter_un

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

La première section de la sortie indique que le test cela_fonctionne de la crate ajouter_un a réussi. La section suivante indique qu'aucun test n'a été trouvé dans la crate additioneur, puis la dernière section indique elle aussi qu'aucun test de documentation n'a été trouvé dans la crate ajouter_un. Lancer cargo test dans un espace de travail structuré comme celui-ci va exécuter les tests pour toutes les crates de cet espace de travail.

Nous pouvons aussi lancer des tests pour une crate en particulier dans un espace de travail à partir du dossier du plus haut niveau en utilisant le drapeau -p et en renseignant le nom de la crate que nous voulons tester :

$ cargo test -p ajouter_un
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running target/debug/deps/ajouter_un-b3235fea9a156f74

running 1 test
test tests::cela_fonctionne ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests ajouter_un

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cette sortie montre que cargo test a lancé les tests uniquement pour la crate ajouter_un et n'a pas lancé les tests de la crate additioneur.

Si vous publiez les crates présentes dans l'espace de travail sur crates.io, chaque crate de l'espace de travail va avoir besoin d'être publiée de manière séparée. La commande cargo publish n'a pas de drapeau --all ou -p, donc vous devrez vous rendre dans chaque dossier de chaque crate et lancer cargo publish sur chaque crate présente dans l'espace de travail pour publier les crates.

En guise d'entrainement supplémentaire, ajoutez une crate ajouter_deux dans cet espace de travail de la même manière que nous l'avons fait pour la crate ajouter_un !

Au fur et à mesure que votre projet se développe, pensez à utiliser un espace de travail : il est plus facile de comprendre des composants individuels, plus petits, plutôt qu'un gros tas de code. De plus, garder les crates dans un espace de travail peut améliorer la coordination entre elles si elles sont souvent modifiées ensemble.