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 | |
java | Le code Java |
resources | Les fichiers à inclure dans le jar |
scala | Le code scala |
test | |
java | Le code Java des tests |
resources | Les fichiers à inclure pour les tests |
scala | Le code Scala pour les tests |
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 :
- Attribuer le nom hello à l’entrée name.
- 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