SBT Partie 3 : Étendre les fonctions

Ceci est la troisième et dernière partie d’une série d’articles consacrée à SBT. Après avoir vu les principes de fonctionnement et comment décrire un projet multiple, nous allons voir ic comment étendre les fonctions de SBT :

  • en ajoutant des tâches,
  • en ajoutant des plugins,
  • en créant nous-même un plugin.

Dans les exemples illustrant cet article, nous continuerons à utiliser la structure de projets présentée dans le second article :

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

Ajouter des tâches

Définition

Lorsque je dis que nous allons ajouter des tâches, c’est en fait un abus de langage. Comme vu dans le premier article, un build SBT est une série de Key. Nous allons donc en réalité ajouter des Key des trois types (Setting, Task et Input).

``` // Definition des Key

// Définition d'un Setting
val sampleSetting = settingKey[String]("Configure something")

// Définition d'un Task
val sampleTask = taskKey[Unit]("Print character's number of project name")

// Définition d'un Input
val sampleInput = inputKey[Unit]("Display number of parameter")

// Implémentation sur le projet Root
lazy val blogIpponMultipleRoot = (project in file(".")).
settings(
name := "blog-ippon-multiple-root", // Implémentation d'un Setting standard de SBT
sampleSetting := "A configuration", // Implémentation du Setting ajouté
sampleTask := { // Implémentation du Task ajouté
println(s"The number of character is : ${name.value.size}") // Le code appelle un autre Setting
},
sampleInput := { // Implémentation du Input ajouté
val args = Parsers.spaceDelimited("").parsed // Récupère les paramètres du Input
println(s"The number of argument is : ${args.size}")
}
)```

### Réutilisation d’une tâche

Il arrive que nous souhaitions avoir la même tâche sur le projet ou les sous-projets. Pour cela, il suffit d’utiliser une variable Scala. Dans l’exemple ci-dessous, la tâche sera appliquée au projet root et au sous-projet service.

``` val sampleTask = taskKey[Unit]("Print character's number of project name")

val sampleTaskImplementation = sampleTask := {
println(s"The number of character is : ${name.value..size}")
}

lazy val blogIpponMultipleRoot = (project in file(".")).
settings(
name := "blog-ippon-multiple-root",
sampleTaskImplementation
)

lazy val blogIpponMultipleService = (project in file("blog-ippon-multiple-service")).
settings(
name := "blog-ippon-multiple-service",
sampleTaskImplementation
)```

### Insertion d’une tâche

Il peut être intéressant d’ajouter l’exécution d’une tâche dans build complet, par exemple, si nous souhaitons générer du code avant la compilation. Pour ce faire, nous allons modifier la tâche compile pour y insérer l’appel à notre tâche.

``` val sampleTask = taskKey[Unit]("Print character's number of project name")

val sampleTaskImplementation = sampleTask := {
println(s"The number of character is : ${name.value..size}")
}

lazy val blogIpponMultipleService = (project in file("blog-ippon-multiple-service")).
settings(
name := "blog-ippon-multiple-service",
sampleTaskImplementation,
compile in Compile := { // la Task compile est différente en fonction de la configuration, il faut préciser celle-ci
sampleTask.value // Appel à notre tâche
(compile in Compile).value // Appel à la tâche d'origine pour que notre code compile
}
)```

### Utilisation d’une librairie externe

Supposons que nous ayons développé une librairie permettant de générer un changelog git de ce type conventional changelog (https://github.com/conventional-changelog/conventional-changelog)

Cette librairie aurait pour interface ceci :

``` object ConventionalChangelogGenerator {
def generate(localGitRepository: String, localGenerateDirectory: String, format: LogGeneratorFormat) = Unit

}```

 

Le fonctionnement est simple, il s’agit d’un object contenant une fonction generate prenant en paramètre :

  • localGitRepository : le chemin vers le répertoire .git
  • localGenerateDirectory : le chemin où générer le fichier de sortie
  • LogGeneratorFormat : le format du fichier de sortie qui peut être HTML ou Markdown

Pour pouvoir utiliser la librairie, il suffit de la déclarer dans le fichier project/build.sbt :

`libraryDependencies += "fr.blogippon" %% "sconventionalchangelog-core" % "1.0"`
 

Nous pouvons alors importer l’objet dans le fichier build.sbt du projet et l’utiliser dans une tâche :

``` import fr.blogippon.sconventionalchangelog.ConventionalChangelogGenerator import fr.blogippon.sconventionalchangelog.generator.LogGeneratorFormat

val generateChangeLog = taskKey[Unit]("Generate changelog base on .git directory")

lazy val blogIpponMultipleRoot = (project in file(".")).
settings(
name := "blog-ippon-multiple-root",
generateChangeLog := {
val gitRepositoryDirectory = baseDirectory.value.getAbsolutePath+"/.git"
val targetDirectory = target.value.getAbsolutePath
val format = LogGeneratorFormat.Markdown
ConventionalChangelogGenerator.generate(gitRepositoryDirectory, targetDirectory, format)
}
)


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


## Utiliser un Plugin

<span style="font-weight: 400;">Une autre manière d’étendre les fonctionnalités de SBT est d’utiliser l’un des nombreux plugins disponibles. Pour faire ceci, rien de plus simple, il suffit de déclarer le plugin dans un fichier </span>*<span style="font-weight: 400;">*.sbt</span>*<span style="font-weight: 400;"> se trouvant dans le répertoire </span>*<span style="font-weight: 400;">project</span>*<span style="font-weight: 400;">.</span>

<span style="font-weight: 400;">Pour déclarer le plugin </span>[*<span style="font-weight: 400;">sbt-assembly</span>*](https://github.com/sbt/sbt-assembly)<span style="font-weight: 400;">, il suffit d’ajouter ceci :</span>

<div class="code-embed-wrapper">`<code class="language-java code-embed-code">addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")`

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

Pour la gestion des plugins, il y a deux conventions :

- <span style="font-weight: 400;">Déclarer tous les plugins dans un fichier </span>*<span style="font-weight: 400;">project/plugins.sbt</span>*
- <span style="font-weight: 400;">Créer un fichier </span>*<span style="font-weight: 400;">.sbt</span>*<span style="font-weight: 400;"> par plugin. Par exemple, pour le plugin </span>*<span style="font-weight: 400;">assembly</span>*<span style="font-weight: 400;"> nous utilisons </span>*<span style="font-weight: 400;">project/assembly.sbt</span>*

<span style="font-weight: 400;">La plupart des plugins SBT sont maintenant des </span>*<span style="font-weight: 400;">AutoPlugin</span>*<span style="font-weight: 400;">, c’est-à-dire qu’aucune opération n’est nécessaire pour les activer. Dans certains cas, des plugins nécessitent qu’on les activent. Ceci se fait de la manière suivante :</span>

<div class="code-embed-wrapper">```
<code class="language-java code-embed-code">lazy val blogIpponMultipleRoot = (project in file(".")).
  enablePlugins(AssemblyPlugin).
  settings(
    name := "blog-ippon-multiple-root"
  )```

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

<span style="font-weight: 400;">Nous pouvons aussi désactiver un plugin :</span>

<div class="code-embed-wrapper">```
<code class="language-java code-embed-code">lazy val blogIpponMultipleRoot = (project in file(".")).
  disablePlugins(AssemblyPlugin).
  settings(
    name := "blog-ippon-multiple-root"
  )```

<div class="code-embed-infos"><span class="code-embed-name"></span></div></div>***Remarque****<span style="font-weight: 400;"> : Le plugin assembly permet de créer un gros JAR contenant l’ensemble du code ainsi que ses dépendances. Dans le cas de notre projet, si nous l’appliquons sur le root, alors le plugin générera un JAR vide. Pour récupérer notre application complète, il faut lancer l’assemblage sur le projet Web.</span>*


## Créer un plugin

<span style="font-weight: 400;">Dans la version 0.13.5 de SBT la fonctionnalité auto plugin a été introduite et permet de créer facilement des </span>*<span style="font-weight: 400;">Keys</span>*<span style="font-weight: 400;"> aux projets au travers d’un plugin.</span>

L’exemple ci-dessous, montre comme nous pourrions créer un plugin reprenant les fonctionnalités du *ConventionalChangelog*

<span style="font-weight: 400;">Tout d’abord, nous allons créer un projet SBT standard et d’indiquer via le </span>*<span style="font-weight: 400;">Setting sbtPlugin</span>*<span style="font-weight: 400;"> qu’il s’agit d’un plugin.</span>

<div class="code-embed-wrapper">```
<code class="language-java code-embed-code">organization := "fr.blogippon"

name := "sbt-sconventionalchangelog-plugin"  // Par convention un plugin SBT doit
                                             // débuter par sbt-
version := "1.0"

sbtPlugin := true                           // Indique qu'il s'agit d'un plugin```

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

<span style="font-weight: 400;">Le point d’entrée d’un plugin est un </span>*<span style="font-weight: 400;">object</span>*<span style="font-weight: 400;"> qui étend </span>*<span style="font-weight: 400;">AutoPlugin</span>*

<div class="code-embed-wrapper">```
<code class="language-java code-embed-code">package fr.blogippon.sbtsconventionalchangelogplugin

import fr.blogippon.sbtsconventionalchangelogplugin.LogGeneratorFormat
import sbt._

// Import toutes les Keys standards comme name, target ...
import sbt.Keys._

object ConventionalChangelogGeneratorPlugin extends AutoPlugin {

  // Indique que les Key seront intégrées automatiquement, sinon il est   
  // nécessaire la fonction .enablePlugins sur le projet
  override def trigger = allRequirements

  // Nous définissons notre tâche comme dans le build.sbt
  val generateChangeLog = taskKey[Unit]("Generate changelog base on .git directory")

  // Nous ajoutons la tâche 
  override lazy val projectSettings = Seq(
    generateChangeLog := {
      val gitRepositoryDirectory = baseDirectory.value.getAbsolutePath + "/.git"
      val targetDirectory = target.value.getAbsolutePath
      val format = LogGeneratorFormat.Markdown
      ConventionalChangelogGenerator.generate(gitRepositoryDirectory, targetDirectory, format)
    }
  )

}```

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

<span style="font-weight: 400;">Dans l’exemple ci-dessus, la tâche a été ajoutée au niveau projet, nous pouvons ajouter des tâches :</span>

- <span style="font-weight: 400;">Dans </span>*<span style="font-weight: 400;">globalSettings</span>*<span style="font-weight: 400;"> pour que la tâche soit ajoutée une seule fois, pour par exemple des valeurs par défaut</span>
- <span style="font-weight: 400;">Dans </span>*<span style="font-weight: 400;">buildSettings</span>*<span style="font-weight: 400;"> pour que la tâche soit évaluée une seule fois par scope dans le build quelque soit le nombre de projets </span>
- <span style="font-weight: 400;">Dans </span>*<span style="font-weight: 400;">projectSettings</span>*<span style="font-weight: 400;"> pour que la tâche soit définie sur chaque projet</span>

<span style="font-weight: 400;">Il est possible de rendre des </span>*<span style="font-weight: 400;">Key</span>*<span style="font-weight: 400;"> accessibles dans le </span>*<span style="font-weight: 400;">build.sbt</span>*<span style="font-weight: 400;"> directement sans avoir à passer par un import en définissant un objet </span>*<span style="font-weight: 400;">autoImport</span>*<span style="font-weight: 400;"> comme ci-dessous</span>

<div class="code-embed-wrapper">```
<code class="language-java code-embed-code">package fr.blogippon.sbtsconventionalchangelogplugin

import fr.blogippon.sbtsconventionalchangelogplugin.LogGeneratorFormat
import sbt._

import sbt.Keys._
import sbt._

object ConventionalChangelogGeneratorPlugin extends AutoPlugin {

  override def trigger = allRequirements

  // Ce qui est défini ici sera accessible directement dans le build.sbt
  object autoImport {
    val html = LogGeneratorFormat.Html
    val markdown = LogGeneratorFormat.Markdown
    val changelogFormat = settingKey[LogGeneratorFormat]("Changelog file format (HTML or Markdown)")
  }

  import autoImport._

  val generateChangeLog = taskKey[Unit]("Generate changelog base on .git directory")

  override lazy val globalSettings = Seq(
    changelogFormat := {
      LogGeneratorFormat.Markdown
    }
  )

  override lazy val projectSettings = Seq(
    generateChangeLog := {
      val gitRepositoryDirectory = baseDirectory.value.getAbsolutePath + "/.git"
      val targetDirectory = target.value.getAbsolutePath
      val format = changelogFormat.value
      ConventionalChangelogGenerator.generate(gitRepositoryDirectory, targetDirectory, format)
    }
  )

}```

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

<span style="font-weight: 400;">Pour utiliser notre plugin, nous devons : </span>

- <span style="font-weight: 400;">Le rendre accessible à notre projet :</span>- <span style="font-weight: 400;">En le déployant sur un repository central</span>
- <span style="font-weight: 400;">En exécutant la commande </span>*<span style="font-weight: 400;">sbt publishLocal</span>*<span style="font-weight: 400;"> pour l’ajouter à notre repository local</span>
- Déclarant le plugin dans *<span style="font-weight: 400;">project/plugins.sbt</span>*

<div class="code-embed-wrapper">`<code class="language-java code-embed-code">addSbtPlugin("fr.blogippon" % "sbt-sconventionalchangelog-plugin" % "1.0")`

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

- <span style="font-weight: 400;">Nous pouvons configurer le format dans notre </span>*<span style="font-weight: 400;">build.sbt</span>*

<div class="code-embed-wrapper">```
<code class="language-java code-embed-code">// Aucun import en relation avec le plugin. Tout est activé automatiqument

lazy val blogIpponMultipleRoot = (project in file(".")).
  settings(
    name := "blog-ippon-multiple-root", 
    changelogFormat := html             // on peut surcharger le format 
  )```

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

- <span style="font-weight: 400;">Pour lancer la tâche </span>*<span style="font-weight: 400;">sbt generateChangeLog</span>*


# Conclusion

<span style="font-weight: 400;">Nous avons vu qu’il est simple d’étendre les fonctionnalités de SBT, soit directement dans notre projet, soit via un plugin.</span>

Pour ma part, ce que j’apprécie dans le fonctionnement de SBT est la possibilité d’étendre ses fonctionnalités dans le répertoire *project* à l’extérieur du fichier *build.sbt*. Les développeurs travaillant sur le projet pourront comprendre le *build.sbt* car il reste simple, l’ensemble de la complexité étant caché dans le *project.*

<span style="font-weight: 400;">Dernier point intéressant, le passage d’un build complexe en plugin est extrêmement simple, au final il s’agit de déplacer du code Scala. </span>