SBT Partie 2 : Projet multiple

Dans l’article précédent, nous avons vu le principe de fonctionnement de SBT et comment gérer les cas d’usage les plus simples (écrire un build, gérer les dépendances, gérer les options spécifiques à Scala). Dans cette seconde partie, nous allons voir comment réaliser un projet multiple.

Travailler en mode multi-projets

Lorsqu’on travaille en mode multi-projets sur SBT, le but est de configurer tous les projets dans le même build.sbt. Un build multi-projets peut inclure des projets qui sont situés n’importe où par rapport à lui, mais par convention nous utilisons toujours une organisation hiérarchique de ce type :

  • blog-ippon-multiple-root/- build.sbt
  • blog-ippon-multiple-data/
  • blog-ippon-multiple-service/
  • blog-ippon-multiple-web/
  • project/- build.properties
  • target/

Dans la suite de ce post, les exemples seront sur cette arborescence.

Où et comment écrire son build ?

Dans un projet SBT, la description du build est faite dans le fichier build.sbt, se trouvant à la racine du projet. Dans un projet multiple, le build.sbt du projet root est utilisé, mais le build.sbt de chacun des projets fils peut être utilisé selon les cas et ceci peut être source d’incompréhension.

Par exemple :

  • Ceci est un extrait du fichier build.sbt du projet blog-ippon-multiple-root.
``` lazy val blogIpponMultipleService = (project in file("blog-ippon-multiple-service")). settings( name := "blog-ippon-multiple-service" // Le nom du projet service version := "0.2" )```
 
  • Ceci est un extrait du fichier build.sbt du projet blog-ippon-multiple-service.
`name := "An other name for service project"`
 
  • Pour obtenir des informations, selon le répertoire du projet où vous tapez la commande, vous aurez des résultats différents.
``` ~blog-ippon-multiple-root>sbt blogIpponMultipleService/name [info] An other name for service project

~blog-ippon-multiple-service>sbt name
[info] An other name for service project

~blog-ippon-multiple-root>sbt blogIpponMultipleService/version
[info] 0.2

~blog-ippon-multiple-service>sbt version
[info] 0.1-SNAPSHOT```

 

On voit bien dans l’exemple que les propriétés du build.sbt dans le sous-projet sont prises en compte lorsqu’on lance sbt depuis le root en précisant le sous-projet. Mais lorsqu’on se positionne directement dans le répertoire du sous-projet, les propriétés définies au-dessus ne sont pas prises en compte.

Cette remarque est vraie également si on écrit directement une propriété dans le build.sbt du projet root.

  • Par exemple, si j’ajoute la ligne suivante :
`version := "version from root"`
 
  • J’obtiens ceci :
``` ~blog-ippon-multiple-root>sbt blogIpponMultipleService/version [info] version from root

~blog-ippon-multiple-service>sbt version
[info] 0.1-SNAPSHOT

~blog-ippon-multiple-root>sbt blogIpponMultipleService/version
[info] 0.2


<div class="code-embed-infos"><span class="code-embed-name"></span></div></div> 

<span style="font-weight: 400;">Pour toutes ces raisons, je vous conseille donc :</span>

1. <span style="font-weight: 400;">De n’avoir qu’un seul fichier </span>*<span style="font-weight: 400;">build.sbt</span>*<span style="font-weight: 400;"> à la racine du projet root.</span>
2. <span style="font-weight: 400;">De travailler uniquement avec la structure (</span>*<span style="font-weight: 400;">project in file(“.”)</span>*<span style="font-weight: 400;">).</span>
3. <span style="font-weight: 400;">De lancer vos commandes toujours depuis le répertoire root.</span>

<span style="font-weight: 400;">Au final le fichier </span>*<span style="font-weight: 400;">build.sbt</span>*<span style="font-weight: 400;"> le plus basique aura la forme suivante :</span>

<div class="code-embed-wrapper">```
<code class="language-java code-embed-code">
lazy val blogIpponMultipleRoot = (project in file("."))

lazy val blogIpponMultipleWeb = (project in file("blog-ippon-multiple-web"))

lazy val blogIpponMultipleService = (project in file("blog-ippon-multiple-service"))

lazy val blogIpponMultipleData = (project in file("blog-ippon-multiple-data"))```

<div class="code-embed-infos"><span class="code-embed-name"></span></div></div> 

<span style="font-weight: 400;">Le désavantage de ceci est qu’il est possible que le fichier </span>*<span style="font-weight: 400;">build.sbt</span>*<span style="font-weight: 400;"> grossisse et devienne ingérable. Il ne faut pas oublier que nous pouvons écrire du code Scala et que nous avons le répertoire </span>*<span style="font-weight: 400;">project</span>*<span style="font-weight: 400;"> qui est dédié à l’écriture du build. Nous pouvons ranger ce qui est code réutilisable dans </span>*<span style="font-weight: 400;">project</span>*<span style="font-weight: 400;"> et garder le fichier </span>*<span style="font-weight: 400;">build.sbt</span>*<span style="font-weight: 400;"> pour le code qui décrit le build.</span>


## Comment travailler

<span style="font-weight: 400;">Comme vu précédemment, lorsque vous naviguez dans les répertoires et que nous exécutons une commande, celle-ci va chercher ses configurations dans le répertoire courant et ne va pas chercher à remonter des éléments dans le </span>*<span style="font-weight: 400;">build.sbt</span>*<span style="font-weight: 400;"> à la racine.</span>

<span style="font-weight: 400;">Je vous conseille donc de toujours travailler dans le répertoire root et de lancer les commandes comme ceci :</span>

<span style="font-weight: 400;">Mode batch</span>

<div class="code-embed-wrapper">```
<code class="language-bash code-embed-code">// sbt <nom_val_project>/<nom_task>
// par exemple 
sbt blogIpponMultipleService/version
…
…
[info] 1.0```

<div class="code-embed-infos"><span class="code-embed-name"></span></div></div> 

Mode <span style="font-weight: 400;">interactif</span>

<div class="code-embed-wrapper">```
<code class="language-bash code-embed-code">// <nom_val_project>/<nom_task>
// par exemple
>blogIpponMultipleService/version
[info] 1.0
// on peut se positionner dans un projet 
// project <nom_val_project>
// par exemple 
>project blogIpponMultipleService
[info] Set current project to ...
// ensuite taper la tâche directement
>version```

<div class="code-embed-infos"><span class="code-embed-name"></span></div></div> 


## Common settings

<span style="font-weight: 400;">Dans un projet multiple, il est nécessaire de pouvoir partager des données. Pour ce faire, nous pouvons utiliser des variables Scala et les associer à chaque projet comme ci-dessous.</span>

<div class="code-embed-wrapper">```
<code class="language-java code-embed-code">….
lazy val commonSettings = Seq(
  crossPaths := false,
  organization := "fr.ippon.blog",
  version := "1.0"
)

lazy val blogIpponMultipleRoot = (project in file(".")).
  settings(commonSettings: _*)

lazy val blogIpponMultipleWeb = (project in file("sbt-multiple-web")).
  settings(commonSettings: _*)

lazy val blogIpponMultipleService = (project in file("sbt-multiple-service")).
  settings(commonSettings: _*)

lazy val blogIpponMultipleData = (project in file("sbt-multiple-data")).
  settings(commonSettings: _*)
 

Si l’on ne souhaite pas définir l’ensemble des variables Scala dans le build.sbt, il est possible de créer une classe Dependencies à la racine du répertoire project.

``` import sbt.Keys._

object Dependencies {

lazy val commonSettings = Seq(
crossPaths := false,
organization := "fr.ippon.blog",
version := "1.0"
)

}```

 

Il suffit d’ajouter un import dans le build.sbt

``` import Dependencies._ ….

lazy val blogIpponMultipleRoot = (project in file(".")).
settings(commonSettings: _*)

lazy val blogIpponMultipleWeb = (project in file("sbt-multiple-web")).
settings(commonSettings: _*)

lazy val blogIpponMultipleService = (project in file("sbt-multiple-service")).
settings(commonSettings: _*)

lazy val blogIpponMultipleData = (project in file("sbt-multiple-data")).
settings(commonSettings: _*)```

 

Dependencies

Librairies

Pour gérer les dépendances à des librairies, il suffit d’ajouter celles-ci à la propriété libraryDependencies de chacun des projets. Il est possible d’utiliser une variable Scala pour partager les données entre plusieurs projets. On peut utiliser commonSettings :

``` ….

lazy val guava = "com.google.guava" % "guava" % "18.0"

lazy val commonDependencies = Seq(guava)

lazy val commonTestDependencies = Seq(scalaTest, mockito, assertJ)

lazy val commonSettings = Seq(
crossPaths := false,
organization := "fr.ippon.blog",
version := "1.0",
libraryDependencies ++= commonDependencies,
libraryDependencies ++= commonTestDependencies
)

lazy val blogIpponMultipleRoot = (project in file(".")).
settings(commonSettings: _*)

lazy val blogIpponMultipleWeb = (project in file("sbt-multiple-web")).
settings(commonSettings: _*).
settings(
libraryDependencies ++= Seq(
springBootWeb
)
)

lazy val blogIpponMultipleService = (project in file("sbt-multiple-service")).
settings(commonSettings: _*)

lazy val blogIpponMultipleData = (project in file("sbt-multiple-data")).
settings(commonSettings: _*)```

 

Projet

Les propriétés et tâches ne s’appliquant qu’au projet en cours, il est nécessaire d’indiquer dans un projet multiple quelles sont les dépendances entre chaque sous-projet. Pour ce faire, il faut utiliser la fonction dependsOn :

``` ….

lazy val blogIpponMultipleWeb = (project in file("sbt-multiple-web")).
settings(commonSettings: _*).
settings(
libraryDependencies ++= Seq(
springBootWeb
)
).dependsOn(blogIpponMultipleService)

lazy val blogIpponMultipleService = (project in file("sbt-multiple-service")).
settings(commonSettings: _*)


<div class="code-embed-infos"><span class="code-embed-name"></span></div></div> 

<span style="font-weight: 400;">Avec une configuration comme celle-ci, si on compile le projet </span>*<span style="font-weight: 400;">blogIpponMultipleWeb</span>*<span style="font-weight: 400;">, alors le projet </span>*<span style="font-weight: 400;">blogIpponMultipleService</span>*<span style="font-weight: 400;"> sera également compilé. Je n’ai pas souhaité dans cette série d’article faire de comparaison avec Maven, mais je trouve que ce comportement est vraiment intéressant. En effet, avec Maven, dans ce cas si on compile uniquement le projet service, alors Maven va chercher sa dépendance dans le repository. Ceci oblige soit à travailler toujours au niveau du root, soit à d’abord publier la dépendance dans le repository local.</span>

### Aggregate

<span style="font-weight: 400;">Dans certains cas, nous voulons que les tâches s’appliquent sur plusieurs projets, mais sans créer une dépendance entre ces projets. Par exemple, lorsqu’on entre la commande </span>*<span style="font-weight: 400;">clean</span>*<span style="font-weight: 400;"> sur le projet root, on souhaite que tous les projets appliquent celles-ci. Dans ce cas, il faut utiliser </span>*<span style="font-weight: 400;">aggregate</span>*<span style="font-weight: 400;">.</span>

<div class="code-embed-wrapper">```
<code class="language-java code-embed-code">….

lazy val blogIpponMultipleRoot = (project in file(".")).
  aggregate(blogIpponMultipleService, blogIpponMultipleData, blogIpponMultipleWeb)
 

Il est possible de configurer aggregate en le désactivant pour certaines tâches

``` ….

lazy val blogIpponMultipleRoot = (project in file(".")).
aggregate(blogIpponMultipleService, blogIpponMultipleData, blogIpponMultipleWeb).
settings(
aggregate in compile := false
)```

 

Conclusion

Le fonctionnement de SBT est intéressant, notamment le fait que lorsque l’on compile un sous-projet l’ensemble des dépendances est compilé sans passer par le *repository local. *

Attention toutefois, il n’est pas possible de faire un héritage entre un projet père et un projet fils. Pour partager des tâches entre projets, il faut étendre les fonctions de SBT. Ce sera l’objet du dernier article de la série.