Publié par

Il y a 9 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-* et package-* avec plusieurs déclinaisons (all, docs, project, failed, quick...),
  • console et console-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 dans main 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) et ivyRepositories.foreach(println) ;
  • voir le classpath de compilation et celui des test : compileClasspath et testClasspath ;
  • ou bien encore voir les dépendances de projet : mainDependencies.external, mainDependencies.libraries et mainDependencies.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 !

Publié par

Publié par Romain Maton

Après trois années passées chez Improve Technologies, Romain est aujourd'hui consultant Java/JEE chez Xebia. Mission après mission, il s’est forgé une solide expérience en développement Web et logiciel. Il dispose ainsi d'une très large compétence sur l'emsemble de l'ecosystème JEE, que ce soit sur les bonnes pratiques d'architecture, sur les frameworks de développement ou sur les interfaces client riches. Inconditionnel du pair programming, certifié Scrum Master, c'est un fervent disciple des méthodes Agiles. Romain est aussi un contributeur actif de la revue de presse Xebia. Il traite de nombreux sujets tels que RIA, API Java, frameworks ou bien encore Scala.

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.