Tutoriel pour comprendre SBT, un outil de build pour l'écosystème Java et Scala

| Xavier Bucchiotty | Fabian Gutierrez | Xebia |

Ce tutoriel est une introduction à SBT, un outil de build utilisé par le langage Scala et Java.

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

- Titre : Tutoriel pour comprendre SBT, un outil de build pour l'écosystème Java et Scala

- Auteurs : Xavier Bucchiotty, Fabian Gutierrez, Xebia

- Parution : 6 avril 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. Un peu d'histoire

- III. Des alternatives

- IV. Build autosuffisant

- V. Convention over configuration

- VI. SBT, the interactive build tool

- VII. Définition du build avec SBT

- VIII. Sucre syntaxique

- IX. Conclusion

- X. Remerciements

I - Introduction

Si vous utilisez Play Framework!, Spark ou que vous avez des projets en Scala, vous avez sûrement croisé l'outil de build de référence de cet écosystème, SBT. Si vous êtes habitué à Apache Maven, vous risquez d'être surpris les premières fois que vous lancerez des tâches. Pourtant, SBT se révèle être un outil de qualité pour des projets, aussi bien en Java que Scala. Nous vous proposons une série d'articles pour découvrir par la pratique comment démarrer un projet SBT, jusqu'à la création de tâches personnalisées et le packaging des plugins.

II - Un peu d'histoire

Maven a été l'outil de référence pour la construction de projets dans l'écosystème Java. Il présente de gros avantages par rapport aux alternatives plus basiques comme Ant. Avec Maven, nous n'avons plus besoin d'écrire les commandes de compilation et de packaging de nos projets. Il apporte une organisation des répertoires standardisée et permet de définir et télécharger les dépendances directement depuis des serveurs dédiés et publiquement disponibles.

III - Des alternatives

SBT est donc un outil de build pour la JVM, à l'instar de Maven ou Gradle. À partir d'un projet, il permet, entre autres, de gérer ses dépendances, de compiler, d'exécuter des tests et de publier les artefacts sur des repositories.

Voici la description d'un projet minimal qui permet de faire une comparaison parmi ces trois alternatives :

Sbt

name := "A Project"
version := "1.0"
scalaVersion := "2.9.1"
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.1.2"

gradle

apply plugin: 'java'
version = '1.0'
sourceCompatibility = 1.7
targetCompatibility = 1.7
dependencies {
compile 'ch.qos.logback:logback-classic:1.1.2'
}

maven

<?xml version="1.0" encoding="UTF-8">
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.xebia</groupId>
<artifactId>a_project</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
</project>

Au premier abord, SBT n'est pas forcément le plus simple :

- la syntaxe à utiliser est pleine de symboles ;

- le fichier de description est écrit en Scala ;

- il est compilé donc plus long à exécuter.

Pourtant, c'est un outil puissant que l'on affectionne de plus en plus. Afin de comprendre ce qui le différencie de ses confrères, plongeons-nous dans ses concepts de base.

IV - Build autosuffisant

Si vous souhaitez utiliser SBT, rien de bien farfelu, il suffit de le télécharger et de l'installer sur votre poste. Premier point intéressant, chaque projet peut spécifier sa version de SBT. Il suffit ensuite d'au moins une version installée. En lançant SBT sur le projet, il vérifiera les contraintes sur la version à utiliser et la téléchargera si nécessaire.

Cela est particulièrement intéressant dans le contexte d'une usine de build dans une entreprise. Plutôt que d'imposer la même version pour tout le monde et ainsi créer de l'inertie pour la montée de version des outils, tout se fera de façon isolée et simple sans intervention manuelle particulière.

SBT en fera de même avec la version de Scala ; il est possible de la spécifier dans le projet. Si elle est absente, SBT la téléchargera pour ce projet.

Nous verrons plus tard comment positionner ces variables dans la configuration de votre projet. Nous trouvons appréciable l'idée d'avoir un build qui soit reproductible car tout est spécifié dans le projet, même la version de la plateforme et du SDK. Il est important de noter que Gradle fournit aussi ce genre de mécanisme avec l'outil Gradle Wrapper.

V - Convention over configuration

SBT ne réinvente rien et reprend cette définition. Vous avez un projet Java sans dépendance particulière, avec un programme exécutable dans un package situé dans src/main/{java|scala} ? En ligne de commande, tapez simplement sbt run. Même s'il n'y a pas de fichier de configuration pour SBT, il tentera de :

- trouver des sources qui suivent la convention de répertoires puis les compiler ;

- chercher une classe avec ‘main' puis l'exécuter.

Vous avez des tests Junit ? Ajoutez dans un fichier build.sbt à la racine du projet la ligne suivante :

libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test

Lancez la commande sbt test et vos tests s'exécuteront. Mettre en place la configuration minimale d'un projet est un jeu d'enfant.

VI - SBT, the interactive build tool

Contrairement à Maven ou Gradle, SBT est un REPL (Read Eval Print Loop). Il peut donc se lancer comme précédemment avec une tâche sbt [tâche]… ou sans (sbt à la racine du projet). Dans ce cas, la définition du projet est chargée, une JVM est lancée pour SBT mais elle est interactive ! Vous pouvez donc lancer une compilation avec compile, des tests avec test ou publier vos artefacts avec publish sans avoir à recharger votre projet.

Il gère ainsi très bien la compilation incrémentale de Scala. Encore mieux ? Chaque tâche peut être incrémentale ! Si test lance les tests une fois, ~test le refera à chaque changement dans les fichiers du projet. Le symbole ‘~' peut préfixer n'importe quelle tâche. ~run peut ainsi démarrer l'application à chaque changement dans une classe ! C'est un élément de différenciation majeur avec ses confrères. Une fois qu'on y a goûté, il est difficile de revenir en arrière. Cela fournit un environnement interactif au développeur, très agréable dans le travail quotidien.

Le REPL est plutôt intelligent, il comprend la recherche historique des tâches précédentes (CTRL+R), la complétion (TAB) pour les tâches ainsi que les paramètres de TOUTES les tâches. Un exemple :

- si ‘test' permet de lancer tous les tests, ‘testOnly' permet de n'en lancer qu'un seul ;

- si dans mon projet, j'ai deux classes de test fr.xebia.sbt.FirstTest et fr.xebia.sbt.SecondTest, en tapant dans le REPL testOnly fr. puis TAB, SBT me proposera un choix pour compléter (auto-completion) sur ces classes de test ;

- encore mieux, ‘testQuick' permet de lancer uniquement les tests impactés par la dernière phase de compilation.

VII - Définition du build avec SBT

C'est à ce moment que la syntaxe devient déroutante. Contrairement à Maven, mais à l'instar de Gradle, la définition du build d'un projet dans SBT se fait au travers de code et non de XML. La définition des paramètres de base se fait dans un fichier build.sbt à la racine du projet.

Voici la ligne qu'il faut y ajouter pour spécifier la version de Scala du projet :

scalaVersion := "2.11.1"

Si je veux ajouter une bibliothèque Java pour la compilation du code de production, voici la ligne à ajouter dans le fichier de configuration :

libraryDependencies += "org.elasticsearch" % "elasticsearch" % "1.4.0"

On retrouve la définition classique d'une dépendance : groupId, artifactId et version. Les bibliothèques Scala sont toujours publiées avec le numéro de version majeure de la bibliothèque dans l'artifactId, pour contrer les différences entre versions du SDK. Ainsi, pour ajouter Akka en Scala, voici ce qu'il faut ajouter :

libraryDependencies += "com.typesafe.akka" % "akka-actor_2.11" % "2.3.8"

SBT est fait pour l'environnement Scala, un petit sucre syntaxique permet de ne pas avoir à ajouter cela à chaque fois. Il suffit magiquement de doubler le premier ‘%' pour avoir :

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.3.8"

Vous souhaitez ajouter Scalatest comme bibliothèque de tests ?

Alors voilà un nouvel élément !

SBT est configuré via du code qui est compilé. Tentez donc d'écrire la ligne suivante :

libraryDependencies += "org.scalatest"

~/Projects/xxx/build.sbt:7: error: No implicit for Append.Value[Seq[sbt.ModuleID], String] found, so String cannot be appended to Seq[sbt.ModuleID]
libraryDependencies += "org.scalatest"
^
[error] Type error in expression
Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore?

VIII - Sucre syntaxique

Au lancement, SBT compile toute la définition. Ici, il semblerait que nous tentions d'ajouter une String alors qu'il attendait une instance de la classe sbt.ModuleID. C'est plutôt clair. Comme cela ne compile pas, le build ne se lance même pas. En fait, les symboles ‘%' sont des méthodes ajoutées dynamiquement sur la classe String pour permettre de construire des instances de ModuleID à partir des éléments unitaires. C'est au final l'équivalent de :

libraryDependencies += ModuleID("org.scalatest", "scalatest", "2.2.1")

Cela commence à être plus clair ?

Quel est l'avantage d'avoir une définition de build en code par rapport à un fichier XML ? Tout simplement parce que coder en scala est possible dans le build ! Un exemple ? Il existe plusieurs modules dans Akka, je voudrais m'assurer qu'ils aient tous la même version :

val version = "2.3.8"

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % version

libraryDependencies += "com.typesafe.akka" %% "akka-test" % version % "test"

Le groupId est en double ?

val version = "2.3.8"

val akka = "com.typesafe.akka"

libraryDependencies += akka %% "akka-actor" % version

libraryDependencies += akka %% "akka-test" % version % "test"

Il faut ajouter une bibliothèque particulière dans certains cas ?

if (maCondition) {
libraryDependencies += [specific_library]
}

Maven propose le même type de mécanisme mais il est spécifique et limité parce que l'utilisation des profils et des variables revient toujours à faire du XML et des placeholders ; c'est donc souvent difficile à bien gérer.

IX - Conclusion

Dans cet article, nous avons eu une introduction à SBT avec ses avantages par rapport à Maven et Gradle. Nous avons aussi vu la gestion des dépendances et les avantages de la gestion des builds en utilisant du code. Lors du prochain article de la série, nous parlerons de la gestion des tâches et comment nous pouvons les utiliser dans nos projets.

SBT est disponible en téléchargement depuis son site, depuis brew sur MacOS, RPM ou DEB pour Linux, avec en prime une bonne dose de documentation en ligne.

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 KartSeven pour sa correction orthographique et Mickael Baron pour la mise au gabarit.