Tutoriel pour comprendre les concepts clés de SBT : Keys et Settings

| Xavier Bucchiotty | Fabian Gutierrez | Xebia |

Dans notre précédent article, nous avons décrit les principales fonctionnalités de l'outil de build SBT. Ce tutoriel se propose de présenter les concepts clés de SBT : Keys et Settings.

N'hésitez pas à donner votre avis sur ce tutoriel sur le forum Java : Commentez .

- Titre : Tutoriel pour comprendre les concepts clés de SBT : Keys et Settings

- Auteurs : Xavier Bucchiotty, Fabian Gutierrez, Xebia

- Parution : 31 mai 2017

- Licence : Copyright ® 2017 Xebia. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Keys

- III. SettingKey

- IV. TaskKey

- V. InputKey

- VI. Définir ses propres Keys

- VII. Settings

- VIII. Des projets et des settings

- IX. Conclusion

- X. Remerciements

I - Introduction

Dans notre précédent article, nous avons décrit les principales fonctionnalités de l'outil de build SBT. Son approche est bien différente de celle proposée par Apache Maven, car la définition de la construction d'un projet avec SBT est décrite avec du code Scala, ce qui rend l'outil extrêmement flexible. Mais cette flexibilité ne vient pas forcément sans standard. Il existe une structure de projet par défaut qui donne un cadre commun à tous les projets. Ensuite, cette structure est facilement adaptée à chaque besoin grâce à un DSL qui offre à la fois concision et puissance.

Pour comprendre SBT et bien l'utiliser par la suite, nous allons voir ensemble les concepts clés. Il est temps maintenant de parler de Keys et de Settings.

II - Keys

Pour simplifier, nous pouvons voir la définition de construction d'un projet dans SBT comme une Map[Key,Setting] (ou Map<Key, Setting> pour les Javaïstes qui nous lisent). Une instance de Key représente le nom d'une tâche, par exemple test, compile ou d'une propriété comme name, scalaVersion, ou libraryDependencies. Tout ce qui peut être saisi dans la REPL est donc une Key.

Il existe trois types de Key :

- settingKey ;

- taskKey ;

- inputKey.

SBT est une Map[Key,Setting], et même une map immuable ! Une fois chargée, la définition du projet ne peut être changée ! Les trois différents types de Key permettent de définir le cycle de vie des valeurs.

III - SettingKey

Une clé de type SettingKey correspond à une valeur qui est évaluée au chargement du projet. Sa valeur ne changera donc jamais une fois SBT lancé. Cela est utile pour des choses fixes comme le nom du projet, ses dépendances ou encore la version de Scala.

Nous allons maintenant vérifier cela dans SBT. Il existe une tâche inspect qui permet d'afficher la définition d'une Key. $ inspect libraryDependencies permet ainsi d'avoir des informations sur cette Key.

1. [...] $ inspect libraryDependencies

2. [info] Setting: scala.collection.Seq[sbt.ModuleID] = List(org.scala-lang:scala-library:2.10.4, org.scalatest:scalatest:2.2.1:test)

3. [info] Description:

4. [info] Declares managed dependencies.

Une clé est typée. libraryDependencies est de type SettingKey[SettingKey[Seq[sbt.ModuleID]]] et vaut pour ce projet List(org.scala-lang:scala-library:2.10.4, org.scalatest:scalatest:2.2.1:test).

IV - TaskKey

TaskKey représente une Key qui sera évaluée à chaque appel. Typiquement, compile est une TaskKey. Si cela était un SettingKey, la compilation aurait lieu une seule fois au chargement du projet, ce qui ne serait vraiment pas pratique.

1. [...] $ inspect compile

2. [info] Task: sbt.inc.Analysis

3. [info] Description:

4. [info] Compiles sources.

Pour bien saisir la différence, imaginez qu'il existe une Key nommée time qui permet d'avoir la date et l'heure du jour. Si time est définie en tant que SettingKey, sa valeur sera évaluée au chargement du projet. Tout appel à cette tâche dans une même session de SBT donnera toujours la même valeur. Au contraire, si time est définie comme TaskKey, chaque appel retournera la date à l'instant de l'appel.

Ainsi le résultat des tests ou d'une compilation sera en TaskKey, le nom d'un projet, une version ou la liste des bibliothèques sera en SettingKey.

V - InputKey

Le dernier type de Key est InputKey. De la même manière que TaskKey, son contenu est évalué à chaque appel. Cependant, il est possible de lui passer des paramètres. Si la Key test est une TaskKey, testOnly est une InputKey. Cette dernière permet de ne lancer les tests que sur une seule classe, un seul test, un package ; tout cela avec une complétion dans la REPL.

1. [...] $ inspect testOnly

2. [info] Input task: Unit

Pour résumer, voici l'expression de chaque type de Key en équivalent avec du code Scala :

SettingKey : valeur immuable définie au démarrage du projet :

val time = Instant.now()

TaskKey : fonction sans paramètre qui retourne une valeur qui sera calculée à chaque appel :

def time = Instant.now()

InputKey : fonction avec paramètres qui retourne une valeur qui sera calculée à chaque appel :

def time(c:Clock) = Instant.now(c)

VI - Définir ses propres Keys

Il est très facile de créer ses propres Keys au sein de votre projet SBT :

1. // Déclaration d'une SettingKey

2. val startTime = settingKey[String]("Give the start time of the project")

4. // Déclaration d'une TaskKey

5. val now = taskKey[String]("Give the current time")

7. // Déclaration d'une InputKey

8. val onceUponATime = inputKey[Unit]("Give a time")

On retrouve donc le nom de la clé, son type, le type de la donnée et une description. Mais déclarer ces lignes dans un fichier build.sbt ne suffit pas pour l'utiliser. Il nous faut maintenant créer des instances pour ces Keys.

VII - Settings

Si une Key est le nom d'une propriété, le Setting représente sa valeur. La façon la plus simple de déclarer un Setting est d'associer une valeur à une Key. En mathématique, le symbole affectation s'écrit :=. Comme SBT fait le pari d'utiliser un DSL, l'utilisation de ce symbole est pratique et sémantiquement juste.

Voici comment déclarer un nouveau SettingKey et lui associer un Setting comme valeur :

1. // myKey est un SettingKey de String. La description est donnée ici en paramètre de la construction.

2. val myKey = settingKey[String]("This is myKey")

4. // Tentative ici de Smiley Oriented Programming

5. val mySetting = myKey.:=("Hello")

7. // Grâce au langage Scala, cette ligne est équivalente à la précédente

8. val myOtherSetting = myKey := "hello"

Il existe donc une méthode:=sur SettingKey qui retourne un Setting, comme le montre l'extrait suivant issu des sources de SBT :

1. sealed abstract class SettingKey[T]{

2. def :=(v : T) : sbt.Def.Setting[T] = ...

3. }

Dans ce cas, comme myKey est de type SettingKey, le Setting correspondant sera évalué au chargement du projet. Le code d'implémentation d'une Key peut être plus complexe comme dans l'exemple suivant :

1. // setting key. Ici la version du commit Git du projet est évaluée au chargement du projet.

2. // Pour faire une évaluation à chaque appel, il suffit de remplacer settingKey par taskKey

3. val versionGit = settingKey[String]("version de git")

5. // versionSetting est donc de type Setting

6. val versionSetting = versionGit := {

7. val gitCommand = Process(Seq("git","rev-parse","HEAD"))

8. gitCommand !!

9. }

Ce code permet de lancer en ligne de commande git rev-parse HEAD et permet de retourner le résultat du processus afin d'avoir l'identifiant de version courante de Git au sein du dépôt. Le procédé est le même pour une TaskKey, il faut seulement ajouter les analyseurs syntaxiques d'options. Cette partie n'est pas couverte dans cet article.

VIII - Des projets et des settings

Créer des Settings et des Keys, c'est bien, les utiliser, c'est encore mieux. SBT considère la définition du build d'un projet comme une liste de Settings qui peuvent être appelés par leur Key :

1. trait Build {

3. ...

5. def settings: Seq[Setting[_]]

7. ...

9. }

11. // https://github.com/sbt/sbt/blob/1.0.x/main/src/main/scala/sbt/Build.scala#L14

Ainsi, le fichier build.sbt à la racine est simplement une suite de Settings :

1. // build.sbt

2. scalaVersion := "2.11.4"

4. // Le compilateur dans SBT transformera cela en

6. val myScalaVersion: Def.Setting[String] = sbt.Keys.scalaVersion.:=("2.11.14")

7. project(...).settings(myScalaVersion)

Et c'est à peu près tout ! Vous avez maintenant les bases pour personnaliser votre environnement de développement avec SBT. Pour cela, il vous faut :

- déclarer une Key (settingKey, taskKey, inputKey) ;

- déclarer un Setting pour une Key ;

- ajouter ce Setting dans le projet.

IX - Conclusion

Rien de mieux que la manipulation pour apprendre. Dans un répertoire temporaire, créez un fichier build.sbt avec le contenu suivant :

1. // build.sbt

3. val hello = taskKey[Unit]("Say hello")

5. hello := println("Hello, world!")

Lancez SBT dans ce répertoire, puis appelez votre toute nouvelle tâche hello. Vous pourrez constater qu'en saisissant hel dans SBT puis « TAB », vous bénéficiez déjà de la complétion. Vous pouvez reprendre l'exemple de la version Git ou encore l'affichage de la date avec time.

X - Remerciements

Cet article a été publié avec l'aimable autorisation de la société Xebia qui est un cabinet de conseil parisien spécialisé dans les technologies Big Data, Cloud, Web, les architectures Java et la mobilité dans les environnements agiles.

Nous tenons à remercier Claude Leloup pour sa correction orthographique et Mickael Baron pour la mise au gabarit.