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

Pour toutes ces raisons, je vous conseille donc :

  1. De n’avoir qu’un seul fichier build.sbt à la racine du projet root.
  2. De travailler uniquement avec la structure (project in file(“.”)).
  3. De lancer vos commandes toujours depuis le répertoire root.

Au final le fichier build.sbt le plus basique aura la forme suivante :

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"))

Le désavantage de ceci est qu’il est possible que le fichier build.sbt grossisse et devienne ingérable. Il ne faut pas oublier que nous pouvons écrire du code Scala et que nous avons le répertoire project qui est dédié à l’écriture du build. Nous pouvons ranger ce qui est code réutilisable dans project et garder le fichier build.sbt pour le code qui décrit le build.

Comment travailler

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 build.sbt à la racine.

Je vous conseille donc de toujours travailler dans le répertoire root et de lancer les commandes comme ceci :

Mode batch

// sbt <nom_val_project>/<nom_task>
// par exemple 
sbt blogIpponMultipleService/version
…
…
[info] 1.0

Mode interactif

// <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

Common settings

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.

….
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: _*)

Avec une configuration comme celle-ci, si on compile le projet blogIpponMultipleWeb, alors le projet blogIpponMultipleService 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.

Aggregate

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 clean sur le projet root, on souhaite que tous les projets appliquent celles-ci. Dans ce cas, il faut utiliser aggregate.

….

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.