Il y a 13 ans -
Temps de lecture 12 minutes
SBT (simple-build-tool) pour Scala
Maintenant que vous êtes tous convaincus par Scala, nous allons regarder durant les prochaines semaines quelques outils et frameworks indispensables pour démarrer nos projets d’entreprise. En effet, tout comme dans nos projets Java, il n’est plus envisageable au jour d’aujourd’hui de commencer un projet sans un environnement minimum : un bon IDE, un outil de build, de l’intégration continue, un outil de couverture de tests et bien d’autres. Leurs buts : nous faciliter le développement et nous avertir d’éventuels problèmes dans notre code (manque de tests, trop de warnings…).
Le sujet de cet article n’est autre que le framework Scala qui monte (très vite) en ce moment à savoir sbt (pour simple-build-tool). Nous verrons dans cet article ce qu’est sbt, ses différentes fonctionnalités et en quoi cet outil va nous être très utile dans nos développements quotidiens.
Yet Another Build Tool ?
sbt est un outil de build qui se veut simple d’utilisation. Son objectif est de fournir des fonctionnalités basiques et avancées très simples à implémenter sur un projet Scala ou Java.
On pourra dès lors n’utiliser notre IDE qu’en tant qu’éditeur de texte et laisser à sbt les tâches de compilation, de tests unitaires sur une ou plusieurs versions de Scala (très utile pour les concepteurs d’API), changer de version de Scala en cours de développement…
La configuration de se fait en Scala à l’aide d’un DSL qui vous permettra de spécifier ses dépendances de projet, ses plugins et beaucoup d’autres paramètres. Le gestionnaire de dépendances utilisé est ivy. Il est possible pour son projet de spécifier un repository maven ou autre et ainsi n’avoir qu’un seul repository centralisé sur sa machine.
A notez que sbt tourne au minimum sur Java 5.
Fonctionnalités
Générales
L’outil propose plusieurs fonctionnalités en standard dont :
- Configuration du projet en Scala,
- Compilations et tests en continu,
- Support de projets Java et Scala,
- Génération de la
scaladoc
, - Frameworks ScalaCheck, Specs et ScalaTest supportés par défaut,
- Démarrage du REPL avec projet et dépendances dans le classpath,
- Exécution de tâches en parallèle (dont les tests),
- Gestion de dépendances inline, ivy ou maven.
Nous allons regarder plus en détail les actions possibles dans sbt, la configuration d’un projet sbt ainsi que les quelques cas d’utilisation.
Actions
Il y a plusieurs manières d’utiliser sbt. Soit en lançant la console sbt pour effectuer nos actions unitairement dans l’environnement sbt, soit en mode batch avec certaines commandes prédéfinies dans l’appel à sbt. Il est aussi possible (nous le détaillerons plus tard dans l’article) de mettre une action en attente de modification de fichiers pour ensuite être lancée.
// Console $ sbt > compile > test > ... // Batch $ sbt clean compile "get sbt.version" // Wait $ sbt > ~ test
Il est possible de lancer une action sur une version de Scala spécifique. Ainsi, si l’on souhaite par exemple compiler notre projet en Scala 2.8.0 alors que notre projet est actuellement en Scala 2.7.7, nous ferons :
// 2.7.7 by default > compile // Force 2.8.0.Beta1 version > ++2.8.0.Beta1 compile
Les actions possibles sont assez nombreuses sachant que vous connaissez déjà bon nombre d’entre elles :
clean
,compile
,exec
,jetty-run
,package
,test
… qui font ce que vous savez ;),update
pour mettre à jour vos dépendances,sh
qui invoque le shell unix pour par exemple exécuter un find,- nombreuses actions sur
test-*
etpackage-*
avec plusieurs déclinaisons (all, docs, project, failed, quick...
), console
etconsole-quick
pour démarrer l’interpréteur Scala (REPL), la différence entre les deux se faisant sur le classpath chargé au démarrage,
Concernant les actions qui touchent à l’environnement sbt, nous avons :
exit
,quit
,help
,info
,debug
,trace
,warn
,error
…,reload
qui permet de recharger les modifications effectuées sur la configuration de notre projet,current
qui nous donne le nom du projet ainsi que le niveau de log actuel,projects
qui liste tous les projets disponibles en cas d’arborescence de projets,; A ; B
qui exécute une action A et qui, si elle réussie, exécute une action B.
Il est bien sûr possible de créer ses propres actions en construisant une tâche Task
de la manière suivante :
lazy val foo = task { log.info("Foo !"); None }
La tâche foo
sera alors créée et disponible dans la console sbt. A noter qu’une syntaxe minuscule et une séparation par tiret est respectée pour les noms de tâche, ainsi une variable nommée fooBar
donnera comme tâche foo-bar
.
Il est possible de définir sur quelle phase du build la tâche doit être lancée et de fournir une description de la tâche :
lazy val jars = task { ... } dependsOn(compile, doc) describedAs("Package classes and API docs.")
Il est aussi possible de modifier des actions existantes comme clean
, test
ou bien encore package
mais aussi de modifier la dépendance de ces tâches par rapport à d’autres tâches. Vous pouvez aussi faire de l’exécution conditionnelle pour des tâches générant des fichiers. Différentes stratégies existent, on pourra ainsi définir qu’une tâche ne doit être exécutée que si les fichiers cibles qu’elle génère sont périmés ou bien constamment générer ces fichiers cibles.
Configuration
L’arborescence d’un projet sbt est la suivante :
lib/ lib_managed/ project/ boot/ build/ MyProjectConf.scala plugins/ Plugins.scala build.properties src/ main/ scala/ resources/ test/ scala/ resources/ target/
On remarque la même arborescence de dossier que pour un projet maven à savoir src/main/scala
et src/main/resources
pour les sources et src/test/scala
et src/test/resources
pour les tests. Concernant les autres dossiers, lib
vous permettra de déposer vos librairies, lib_managed
contiendra vos dépendances (si aucun repository n’est spécifié), project/build
votre configuration projet tandis que project/plugins
vous permettra de définir les plugins que votre projet utilisera.
La définition des dépendances et la gestion des plugins se fait en Scala. Voici quelques exemples de définition de dépendances :
lazy val jetty = "org.mortbay.jetty" % "jetty" % "6.1.22" lazy val h2 = "com.h2database" % "h2" % "1.2.121" lazy val junit = "junit" % "junit" % "4.5" % "test" lazy val slf4 = "org.slf4j" % "slf4j-log4j12" % "1.4.1"
Il est possible de définir un projet de type parent qui contiendra plusieurs projets (modules). La configuration pour ce type de projet est la suivante :
import sbt._ class ParentProject(info: ProjectInfo) extends ParentProject(info) { lazy val core = project("core", "Core project") lazy val client = project("client", "Client project", core) // Project based on core lazy val dao = project("dao", "DAO project", new DAOProject(_)) class DAOProject(info: ProjectInfo) extends DefaultProject(info) { // Define here custom dependencies } }
Il est aussi possible de définir sa configuration projet à partir d’un fichier maven pom.xml
ou d’un fichier ivy ivy.xml
. Pour cela, il suffit de les ajouter à la racine de votre projet. Ainsi, sur une commande update
, sbt utilisera le fichier maven ou ivy pour gérer les dépendances du projet.
Les configurations possibles sont nombreuses. Pour une configuration plus avancée, je vous renvoie vers cette page qui détaille exhaustivement tout ce que l’on peut faire en terme de configuration dans un projet sbt, du changement de l’arborescence des dossiers aux options de compilation, package et test.
Cas d’utilisation
Triggered execution
En effet, sbt ne se résume pas qu’à un simple outil de configuration projet. Nous avons vu au début de cet article les actions possibles dans sbt, comme par exemple compile
ou test
. Ce sont des actions one shot qu’il faudra relancer autant de fois que l’on voudra pour, dans ce cas, compiler ou tester.
Mais, en préfixant ces actions par le caractère ~
, sbt se mettra en attente de modification dans le scope de l’action et dès qu’une modification sera effectuée, l’action sera déclenchée, d’où leur nom : triggered execution. Ainsi, si une modification se produit par exemple dans src/main/scala
, compile
et test
seront appelés car ils sont en attente de modifications sur les fichiers sources du projet.
Actuellement, trois types d’actions sont de type triggered execution :
- compilation : avec la compilation continue par
~ test-compile
ou uniquement la compilation des sources dansmain
avec~ compile
; - test : avec
~ test-quick
pour lancer les tests qui n’ont pas encore réussi (nouveau ou failed du tests précédent) ou qui ont été recompilés,~ test-failed
pour lancer uniquement les tests qui n’ont pas encore réussi (mais ne relance pas les tests recompilés comme~ test-quick
) et~ test-only mytestpackage.MyTest
pour lancer les tests de la classe spécifiée ; - web-app : si vous utilisez jetty et une fois jetty lancé (
jetty-run
), la commande~ prepare-webapp
recompilera la webapp pour prendre en compte les modifications. Pour avoir une compilation continue totale, je vous renvoie à la section JRebel qui explique comment prendre en compte JRebel pour scanner certains dossiers à recharger dynamiquement et ainsi éviter les reloads de Jetty.
Cross building
L’autre cas d’utilisation qu’offre sbt est le cross building. Cette fonctionnalité est un véritable atout pour toute API ayant comme cible de nombreuses versions de Scala, surtout si elles ne sont pas binairement compatibles entre elles, comme par exemple Scala 2.7.7 et la prochaine version 2.8.0 (mais c’est aussi le cas entre 2.7.2 et 2.7.4).
Voyons d’abord comment utiliser une librairie packagée différemment pour cette version de Scala. L’exemple ci-dessous montre en premier le fait d’imposer la version de Scala. Le problème est que si l’on change temporairement de version de Scala dans notre projet, il risque de ne pas builder. Le second exemple nous montre que sbt peut récupérer la bonne version de la librairie en fonction de la version de Scala courante de notre projet à l’aide de la dépendance spéciale %%
:
val dispatch = "net.databinder.dispatch" % "dispatch_2.7.7" % "0.7.2" val dispatch = "net.databinder.dispatch" %% "dispatch" % "0.7.2"
Maintenant, nous allons pouvoir construire notre projet dans différentes versions de Scala en très peu de configuration et surtout en un caractère ! La définition se fait directement dans la console sbt de la manière suivante :
$ sbt > set build.scala.versions 2.7.7 2.8.0.Beta1 2.7.5 2.7.3 2.7.2 > reload
Par défaut, sbt utilisera la première version définie comme version par défaut. Vous pouvez bien sûr changer de version à tout moment à l’aide de la commande ++version
.
Et pour le cross building, il suffit de préfixer notre action par +
pour qu’elle soit exécutée sur toutes les versions ciblées par notre projet :
> +package > +publish
Point important : certaines dépendances de votre projet ne seront peut-être pas les mêmes selon les versions de Scala que vous ciblez. Il est donc important de faire un pattern matching sur les API qui pourraient poser problème :
val scalatest = buildScalaVersion match { case "2.7.5" => "org.scala-tools.testing" % "scalatest" % "0.9.5" case "2.7.2" => "org.scalatest" % "scalatest" % "0.9.3" case x => error("Unsupported Scala version " + x) }
Project console
Dernier cas d’utilisation que nous verrons dans cet article, le lancement de la console avec tout le contexte de projet intégralement chargé. Ainsi, console-project
démarre l’interpréteur Scala avec le projet courant chargé. On peut dès lors utiliser tous nos objets et faire quelques tests directement dans la console.
Mais ce mode permet bien plus que l’appel de nos objets, il permet aussi, entre autres, de :
- voir les options de compilation :
compileOptions.foreach(println)
; - voir tous les repository :
repositories.foreach(println)
etivyRepositories.foreach(println)
; - voir le classpath de compilation et celui des test :
compileClasspath
ettestClasspath
; - ou bien encore voir les dépendances de projet :
mainDependencies.external
,mainDependencies.libraries
etmainDependencies.scalaJars
.
Installation
Maintenant que vous en savez plus sur sbt, j’espère que vous avez l’eau à la bouche et que avez envie de jouer avec l’outil :) Nous allons donc voir comment installer la bête. Tout d’abord, rendez-vous sur cette page pour télécharger la dernière version de sbt.
Pour l’installation sous Mac, il suffit d’ajouter un fichier dont le nom sera sbt
au même niveau que votre Jar sbt avec le contenu suivant :
java -Xmx512M -jar dirname $0
/sbt-launch.jar "$@"
Il faudra ensuite ajouter dans votre path le répertoire contenant ce fichier et le Jar sbt. Exemple dans .bash_profile
:
export SBT_HOME=$HOME/dev/scala/sbt export PATH=$SBT_HOME:$PATH
Voilà, c’est installé ! Pour les accros de la ligne de commande, voici un mode d’installation fait pour eux (unix) qui est d’ailleurs expliqué sur le site de sbt qui installe sbt dans /usr/local/bin/
:
$ cd ~ $ wget http://simple-build-tool.googlecode.com/files/sbt-launcher-0.7.3.jar $ sudo mv sbt-launcher-0.5.6.jar /usr/local/bin/sbt-launcher.jar $ echo "java -Xmx512M -jar /usr/local/bin/sbt-launcher.jar "$@"" | sudo tee /usr/local/bin/sbt $ sudo chmod +x /usr/local/bin/sbt
En ce qui concerne l’installation Windows, cela se passe ici.
Nous allons maintenant tester l’installation. Commençons par créer un dossier, déplaçons nous dedans et lançons la commande sbt en répondant s
à la question de création de projet (pour scratch
qui fera une configuration rapide du projet). Faisons ensuite un compile
même si aucune source n’existe actuellement :
$ mkdir foobar-project $ cd foobar-project $ sbt

Build successful ! Ajoutons maintenant un fichier PrintMessage.scala
dans src/main/scala
qui affichera un message dans la console sur la tâche run
:
object PrintMessage { def main(args: Array[String]) = println("Foobar Scala !") }
Et maintenant lançons notre message à l’aide de l’action run
:
$ sbt run

Vous voilà prêt pour utiliser sbt !
Conclusion
Cet outil est déjà très utilisé par plusieurs projets Scala open-source (dont par exemple akka). Les possibilités sont très nombreuses et n’ont pas toutes été énumérées dans le présent article. Je vous renvoie vers le wiki de sbt pour un détail complet des fonctionnalités de l’outil.
Ce qui ressort après quelques heures d’utilisation c’est l’extrême simplicité d’utilisation de l’outil, la simplicité de configuration d’un projet et la puissance d’avoir en tâche de fond les tests en continu. Certaines personnes vont même jusqu’à se désynchroniser totalement de leur IDE qui ne devient dès lors qu’un simple éditeur de texte laissant à sbt la compilation, les tests et l’exécution. D’autres nous expliquent comment configurer Eclipse pour faire du debug sur des tests lancés depuis la console sbt.
Un petit exemple de projet sbt est d’ailleurs disponible sur mon GitHub. A vous de jouer !
Commentaire