SBT Partie 1 : Découverte

SBT est l’outil de build de prédilection des développeurs Scala. Il est utilisé par le framework Play ou par le module activator de Lightbend. Au delà d’une guerre de tranchée dans le mode “c’est moi qui est l’outil de build le plus mieux !”, SBT est rapide à mettre en place. Il offre des fonctionnalités intéressantes et surtout il est simple d’étendre ses fonctionnalités. Un savant mélange de Maven et Ant, une alternative à Gradle, avec du Scala à la place de XML ou de Groovy.

Installation

Rien de bien compliqué, il suffit de suivre les instructions sur le site de SBT.

Structure des répertoires

Dès l’installation on peut tout de suite compiler et lancer un programme. Pour cela il suffit de :

  • Créer une classe dans un répertoire vide
public class Hello {

  public static void main(String[] args){
    System.out.println("Hello");
  }

}
  • sbt compile
...
[info] Compiling 1 Java source to /Users/jpbunaz/workspace/tutorial/sbt/blog/target/scala-2.10/classes…
..

  • sbt run
...
[info] Running Hello
Hello
...

SBT fonctionne par convention et compile les classes se trouvant à la racine. Il cherche également lorsque l’on lance la commande run, une classe exécutable.

Comme nous aimons ranger notre code, SBT suit la convention Maven :

main
    javaLe code Java
    resourcesLes fichiers à inclure dans le jar
    scalaLe code scala
test
    javaLe code Java des tests
    resourcesLes fichiers à inclure pour les tests
    scalaLe code Scala pour les tests
# Exécution

Pour lancer une action dans SBT, il existe le mode batch qui va lancer la tâche et sortir de SBT et il existe le mode interactif qui nous permet d’entrer dans un prompt spécifique à SBT.

Mode batch

Pour lancer une commande avec SBT, il suffit d’entrer la ligne suivante :

sbt clean compile

si une tâche prend des arguments, il suffit de tout mettre entre “

sbt clean compile "myTaksWithArguments arg1 arg2 arg3"

Attention, dans SBT, pour lancer des tâches dans des sous-projets, il faut rester à la racine du projet et taper la commande :

sbt mySubProject/clean

Mode interactif

Pour accéder au mode interactif, il suffit de taper sbt sans aucun argument :

sbt
[info] Set current project to blog (in build file:/Users/jpbunaz/workspace/tutorial/sbt/blog/)
>

On peut ensuite taper une commande :

> compile
[info] Updating {file:/Users/jpbunaz/workspace/tutorial/sbt/blog/}blog...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Java source to /Users/jpbunaz/workspace/tutorial/sbt/blog/target/scala-2.10/classes...
[success] Total time: 2 s, completed 28 oct. 2015 16:52:41
>

Ce mode est intéressant car il va nous permettre d’interagir plus simplement avec SBT, notamment avec de l’autocomplétion :

> comp   // taper la touche de tabulation
compile                   compile:                  compileAnalysisFilename   compileIncremental        compilerCache
compilers                 completions
> comp

Un historique des commandes s’affiche en utilisant les commandes commençant par “!” :

> !
History commands:
   !!    Execute the last command again
   !:    Show all previous commands
   !:n    Show the last n commands
   !n    Execute the command with index n, as shown by the !: command
   !-n    Execute the nth command before this one
   !string    Execute the most recent command starting with 'string'
   !?string    Execute the most recent command containing 'string'
>

Avoir la liste des tâches :

> tasks

This is a list of tasks defined for the current project.
It does not list the scopes the tasks are defined in; use the 'inspect' command for that.
Tasks produce values.  Use the 'show' command to run the task and print the resulting value.

  clean    Deletes files produced by the build, such as generated sources, ...
  compile  Compiles sources.
...
  update   Resolves and optionally retrieves dependencies, producing a report.

More tasks may be viewed by increasing verbosity.  See 'help tasks'.

>

Une des fonctionnalités les plus intéressantes est le mode continu qui permet de lancer en permanence une tâche lorsqu’un changement se produit. C’est très intéressant pour lancer ses tests unitaires en tâche de fond. Pour faire ceci, il suffit de précéder une tâche par “~” :

> ~ test
[info] Updating {file:/Users/jpbunaz/workspace/tutorial/sbt/hello/}util...
[info] Updating {file:/Users/jpbunaz/workspace/tutorial/sbt/hello/}root...
[info] Resolving jline#jline;2.12 ...
[info] Done updating.
[info] Resolving org.apache.derby#derby;10.4.1.3 ...
[info] Compiling 1 Scala source to /Users/jpbunaz/workspace/tutorial/sbt/hello/util/target/scala-2.11/classes...
[info] Resolving jline#jline;2.12 ...
[info] Done updating.
[info] Compiling 1 Scala source to /Users/jpbunaz/workspace/tutorial/sbt/hello/target/scala-2.11/classes...
[success] Total time: 5 s, completed 28 oct. 2015 17:16:48
1. Waiting for source changes... (press enter to interrupt)

Comment ça marche un build SBT ?

Un projet SBT est une Map décrivant le projet et le but d’un build va être de transformer cette Map grâce à des Setting[T] (où T est le type de la valeur dans la Map) qui sont associés à une entrée de la Map. Lorsque nous décrivons un build, nous ne faisons rien de plus que construire une liste de Setting[T].

Admettons que nous partons d’une Map vide et que nous appliquons les Setting suivants :

  1. Attribuer le nom hello à l’entrée name.
  2. Générer un jar portant le nom du projet et l’associé à l’entrée package.

Dans cet exemple, nous voyons que l’étape 2 dépend de l’étape 1, SBT est très malin et il va trier la liste dans le bon ordre pour que l’étape 1 soit exécutée avant l’étape 2

Map() -> Setting 1 -> Map("name" -> "Hello") -> Setting 2 -> Map("name" -> "Hello", "package" -> new File("target/hello.jar"))

Un Key peut être de 3 types :

  • SettingKey[T] : Le code contenu est exécuté une seul fois au chargement du build
  • TaskKey[T] : Le code contenu est exécuté lorsque l’on fait appelle à lui. Une tâche peut avoir des effets de bord, comme par exemple écrire un fichier sur le disque
  • InputKey[T] : Une TaskKey avec des arguments

Attention, une TaskKey n’est exécuté qu’une seul fois par lancement. SBT va regarder toutes les dépendances et il va faire une phase de dé-duplication. Il n’est donc pas possible de faire par exemple une tâche avec l’algorithme suivant :

myTaskWhichDoesntDoWhatWeExcepted

Algorithme

  • J’appelle la tâche clean
  • Je génère des classes
  • J’appelle une nouvelle fois la tâche clean

Résultat

  • Les classes générés à l’étape 2 existe, car la tâche clean est lancée une seul fois avant myTaskWhichDoesntDoWhatWeExcepted

Le dernier point à connaître pour comprendre le fonctionnement de SBT est la notion de scope. Dans la Map générée par SBT, la clé n’est pas seulement le nom, mais le nom + le scope, ceci nous permet d’avoir plusieurs valeurs pour une clé.

Le scope peut être de 3 types :

  • Project : la propriété name a une valeur différente selon le projet où l’on se trouve.
  • Configuration : la propriété est différente si on est en configuration de test ou de compilation.
  • Task : la propriété est différente selon la tâche où l’on se trouve.

Comment on décrit un build ?

Il y a plusieurs façon de décrire un build SBT :

  • Un projet simple en .sbt.
  • Un projet multiple en .sbt.
  • Un projet Scala.

Les builds écrits en Scala se trouvent dans un sous-répertoire project. Ce répertoire est un autre projet SBT contenant le code Scala nécessaire au build, ce qui permet de bien séparer le code métier, du code spécifique au build. Si nous souhaitons réutiliser le code Scala du build pour en faire un plugin, nous avons déjà un projet SBT !

Dans ce post, nous ne décrirons pas de build en Scala, mais nous utiliserons le répertoire project pour insérer le code de nos tâches complexes. La description du build sera fait dans un fichier build.sbt à la racine du projet.

Le build.sbt peut avoir deux formes, selon si nous sommes dans un projet simple ou un projet multiple :

Simple

name := "Mon premier projet"

version := "1.0-SNAPSHOT"

organization := "fr.ippon.blog"

Multiple

lazy val root = (project in file(".")).
  settings(
    name := "Mon premier projet",
    version := "1.0-SNAPSHOT", 
    organization := "fr.ippon.blog"
  )

La définition d’un build SBT, consiste donc à enchaîner une succession de définition de Setting.
Lorsqu’on travaille sur un projet simple, sans étendre les fonctionnalités les Setting seront de type SettingKey[T], nous allons voir les plus importantes.

Gestion des dépendances

Il y a deux modes de gestion des dépendances.

Le mode unmanaged

Il suffit de mettre des jars dans le répertoire lib pour qu’ils soient ajoutés dans le classpath. En jouant avec les scopes, il est possible de définir un répertoire pour les librairies de test, mais nous n’aborderons pas ce point, car qui utilise encore le unmanaged ?

Le mode managed

Dans ce mode, nous allons ajouter des entrées à libraryDependencies, qui est type SettingKey[Seq[ModuleID]]. Il faut donc ajouter des ModuleID, ce qui est simplifié grâce à l’ajout de sucres syntaxiques dans un build.sbt :

val derby: ModuleID = "org.apache.derby" % "derby" % "10.4.1.3" % "test"
                              |              |           |          |
                        organization       name       version     scope

lazy val root = (project in file(".")).
  settings(
    name := "Mon premier projet",
    version := "1.0-SNAPSHOT", 
    organization := "fr.ippon.blog",
    libraryDependencies += derby,   // Une seule à la fois
    libraryDependencies ++= Seq(scalaTest, hibernate)  // Plusieurs d'un coup
  )

Entre l’organisation et le nom, il est possible d’utiliser “%%” à la la place de “%”, pour indiquer que la dépendance doit prendre en compte la version de Scala. Par exemple :

"org.scalatest" %% "scalatest" % "2.2.4" % "test"

avec une version de scala à 2.11, donnera en dépendance Maven

<groupId>org.scalatest</groupId>
<groupId>scalatest_2.11</groupId>
<version>2.2.4</version> 
<scope>test</scope>

Pour gérer les librairies, SBT utilise Ivy. Il est donc possible d’utiliser des numéros de version dynamiques, comme par exemple 2.2.+, pour avoir la version 2.2 la plus récente. Vous pouvez retrouver toutes les possibilités des versions dynamiques ici.
Vous pouvez ajouter des repositories en utilisant la propriété resolvers :

resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"

Options Scala / SBT

scalaVersion := "2.11.4" // Version de scala utilisé dans le projet

crossScalaVersions := Seq( "2.10.4", "2.11.0") 

/* 
Le projet est construit pour chaque version de scala 
si on précède la task avec +  
*/

> + compile
…
> + package

Une option très utile pour gérer les versions hétérogènes de chaque développeur sur son poste de travail est de fixer la version de SBT à utiliser pour l’exécution des tâches. Pour cela, il faut créer un fichier build.properties dans le répertoire project :

sbt.version=0.13.9 

/* 
Quelque soit la version de sbt sur votre poste, la 0.13.9 est utilisée pour faire le build
*/

Conclusion

SBT est un outil puissant pour décrire un build, qui est simple à prendre en main, une fois bien compris le système. SBT offre des avantages, comme :

  • Le fait d’écrire son build en *Scala *ce qui permet d’avoir de l’auto-complémention dans les IDE
  • Le mode interactif
  • La gestion de dépendance avec ivy

Nous verrons dans le prochain article de la série comment créer un projet multiple avec SBT