Quelqu'un peut-il expliquer la bonne façon d'utiliser SBT?

100

Je sors du placard là-dessus! Je ne comprends pas SBT. Là, je l'ai dit, maintenant aidez-moi s'il vous plaît.

Tous les chemins mènent à Rome, et qui est la même chose pour SBT: Pour commencer à utiliser SBTil y a SBT, SBT Launcher, SBT-extras, etc, et puis il y a différentes façons d'inclure et décider sur les dépôts. Y a-t-il un «meilleur» moyen?

Je demande parce que parfois je me perds un peu. La documentation SBT est très complète et complète, mais je ne sais pas quand utiliser build.sbtou project/build.propertiesou project/Build.scalaou project/plugins.sbt.

Ensuite, ça devient amusant, il y a le Scala-IDEet SBT- Quelle est la bonne façon de les utiliser ensemble? Qu'est-ce qui vient en premier, la poule ou l'œuf?

Le plus important est probablement, comment trouver les bons référentiels et versions à inclure dans votre projet? Dois-je simplement sortir une machette et commencer à me frayer un chemin? Je trouve assez souvent des projets qui incluent tout et l'évier de la cuisine, puis je me rends compte que je ne suis pas le seul à me perdre un peu.

À titre d'exemple simple, en ce moment, je démarre un tout nouveau projet. Je souhaite utiliser les dernières fonctionnalités de SLICKet Scalaet cela nécessitera probablement la dernière version de SBT. Quel est le bon point de départ et pourquoi? Dans quel fichier dois-je le définir et à quoi doit-il ressembler? Je sais que je peux faire fonctionner cela, mais j'aimerais vraiment avoir un avis d'expert sur où tout devrait aller (pourquoi cela devrait aller là-bas sera un bonus).

J'utilise SBTpour de petits projets depuis plus d'un an maintenant. J'ai utilisé SBTet ensuite SBT Extras(car cela a fait disparaître certains maux de tête par magie), mais je ne sais pas pourquoi je devrais utiliser l'un ou l'autre. Je suis juste un peu frustré de ne pas comprendre comment les choses s'articulent ( SBTet les référentiels), et je pense que cela sauvera le prochain gars de cette manière beaucoup de difficultés si cela pouvait être expliqué en termes humains.

Jack
la source
2
Que voulez-vous dire exactement par "il y a le Scala-IDE et SBT"? Vous définissez votre projet avec sbt et sbt peut générer un projet ide (eclipse oder intellij). Alors SBT vient d' abord ...
Jan
2
@Jan J'ai mentionné cela parce que Scala-IDE utilise SBT comme gestionnaire de construction. Voir assembla.com/spaces/scala-ide/wiki/SBT-based_build_manager et plus bas dans le post, ils mentionnent "Il n'est pas nécessaire de définir votre fichier de projet SBT." ce que j'ai trouvé déroutant.
Jack
D'accord. Comme j'utilise habituellement intellij (ou sublime) pour éditer scala, je ne le savais pas. Je suppose que le constructeur génère ses propres configurations sbt?
Jan
2
@JacobusR Le fait que l'EDI Scala utilise SBT pour construire les sources de votre projet est un détail d'implémentation, et les utilisateurs n'ont pas à s'inquiéter à ce sujet. Il n'y a vraiment aucune implication. En dehors d'Eclipse, les utilisateurs peuvent créer un projet avec SBT, Maven, Ant, ..., et cela ne fera aucune différence pour l'EDI Scala. Encore une chose, même si vous avez un projet SBT, l'EDI Scala ne s'en soucie pas, c'est-à-dire qu'il ne cherche pas Build.scalaà configurer le classpath, et c'est pourquoi vous avez en fait besoin de sbteclipse pour générer le .classpath Eclipse. J'espère que cela t'aides.
Mirco Dotta
1
@Jan Scala IDE a ajouté à la confusion, et oui, une documentation qui donne une vue d'ensemble sur la mise en place d'un bon environnement de développement Scala et des conseils solides sur les flux de travail de programmation appropriés serait très utile.
Jack

Réponses:

29

Le plus important est probablement, comment trouver les bons référentiels et versions à inclure dans votre projet? Dois-je simplement sortir une machette et commencer à me frayer un chemin? Je trouve assez souvent des projets qui incluent tout et l'évier de cuisine

Pour les dépendances basées sur Scala, j'irais avec ce que les auteurs recommandent. Par exemple: http://code.google.com/p/scalaz/#SBT indique d'utiliser:

libraryDependencies += "org.scalaz" %% "scalaz-core" % "6.0.4"

Ou https://github.com/typesafehub/sbteclipse/ a des instructions sur où ajouter:

addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.0-RC1")

Pour les dépendances basées sur Java, j'utilise http://mvnrepository.com/ pour voir ce qui existe, puis je clique sur l'onglet SBT. Par exemple http://mvnrepository.com/artifact/net.sf.opencsv/opencsv/2.3 indique d'utiliser:

libraryDependencies += "net.sf.opencsv" % "opencsv" % "2.3"

Ensuite, sortez la machette et commencez à vous frayer un chemin. Si vous avez de la chance, vous ne finirez pas par utiliser des jars qui dépendent de certains des mêmes jars mais avec des versions incompatibles. Compte tenu de l'écosystème Java, vous finissez souvent par inclure tout et l'évier de la cuisine et il faut un certain effort pour éliminer les dépendances ou vous assurer de ne pas manquer les dépendances requises.

À titre d'exemple simple, en ce moment, je démarre un tout nouveau projet. Je veux utiliser les dernières fonctionnalités de SLICK et Scala et cela nécessitera probablement la dernière version de SBT. Quel est le bon point de départ et pourquoi?

Je pense que le bon sens est de renforcer progressivement l'immunité contre la sbt .

Assurez-vous de bien comprendre:

  1. format des étendues {<build-uri>}<project-id>/config:key(for task-key)
  2. les 3 saveurs de paramètres ( SettingKey, TaskKey, InputKey) - lire la section intitulée « Touches tâche » dans http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def

Gardez ces 4 pages ouvertes à tout moment afin que vous puissiez sauter et rechercher diverses définitions et exemples:

  1. http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def
  2. http://www.scala-sbt.org/release/docs/Detailed-Topics/index
  3. http://harrah.github.com/xsbt/latest/sxr/Keys.scala.html
  4. http://harrah.github.com/xsbt/latest/sxr/Defaults.scala.html

Utilisez au maximum showet inspect et la complétion des onglets pour vous familiariser avec les valeurs réelles des paramètres, leurs dépendances, les définitions et les paramètres associés. Je ne crois pas que les relations que vous découvrirez en utilisant inspectsont documentées nulle part. S'il y a une meilleure façon, je veux en savoir plus.

huynhjl
la source
25

La façon dont j'utilise sbt est:

  1. Utilisez sbt-extras - récupérez simplement le script shell et ajoutez-le à la racine de votre projet
  2. Créez un projectdossier avec un MyProject.scalafichier pour configurer sbt. Je préfère de loin cela à l' build.sbtapproche - c'est scala et est plus flexible
  3. Créez un project/plugins.sbtfichier et ajoutez le plugin approprié pour votre IDE. Soit sbt-eclipse, sbt-idea ou ensime-sbt-cmd afin que vous puissiez générer des fichiers de projet pour eclipse, intellij ou ensime.
  4. Lancez sbt à la racine de votre projet et générez les fichiers de projet pour votre IDE
  5. Profit

Je ne prends pas la peine de vérifier les fichiers de projet IDE car ils sont générés par sbt, mais il peut y avoir des raisons pour lesquelles vous souhaitez le faire.

Vous pouvez voir un exemple de configuration comme celui-ci ici .

Channing Walton
la source
Merci pour la bonne réponse. J'ai accepté l'autre réponse, car elle couvre plus de terrain, et j'ai voté à la hausse pour votre cause, c'est vraiment bien aussi. J'aurais accepté les deux si j'avais pu.
Jack
0

Utilisez Typesafe Activator, une façon élégante d'appeler sbt, qui est fournie avec des modèles de projet et des graines: https://typesafe.com/activator

Activator new

Fetching the latest list of templates...

Browse the list of templates: http://typesafe.com/activator/templates
Choose from these featured templates or enter a template name:
 1) minimal-java
 2) minimal-scala
 3) play-java
 4) play-scala
(hit tab to see a list of all templates)
Andreas Neumann
la source
5
Je suis partisan de l'idée qu'en cas de doute, ajouter plus de magie au mélange ne résoudra probablement pas vos problèmes.
cubique
0

Installation

brew install sbt ou similaire installe sbt qui consiste techniquement en

Lorsque vous exécutez à sbtpartir du terminal, il exécute en fait le script bash du lanceur sbt. Personnellement, je n'ai jamais eu à m'inquiéter de cette trinité, et j'ai juste utilisé sbt comme si c'était une seule chose.

Configuration

Pour configurer sbt pour un projet particulier, enregistrez le .sbtoptsfichier à la racine du projet. Pour configurer sbt à l'échelle du système, modifiez /usr/local/etc/sbtopts. L'exécution sbt -helpdevrait vous indiquer l'emplacement exact. Par exemple, pour donner plus de mémoire à sbt en tant qu'exécution unique sbt -mem 4096, ou enregistrer -mem 4096dans .sbtoptsou sbtoptspour que l'augmentation de la mémoire prenne effet de manière permanente.

 Structure du projet

sbt new scala/scala-seed.g8 crée une structure de projet sbt Hello World minimale

.
├── README.md  // most important part of any software project
├── build.sbt  // build definition of the project
├── project    // build definition of the build (sbt is recursive - explained below)
├── src        // test and main source code
└── target     // compiled classes, deployment package

Commandes fréquentes

test                                                // run all test
testOnly                                            // run only failed tests
testOnly -- -z "The Hello object should say hello"  // run one specific test
run                                                 // run default main
runMain example.Hello                               // run specific main
clean                                               // delete target/
package                                             // package skinny jar
assembly                                            // package fat jar
publishLocal                                        // library to local cache
release                                             // library to remote repository
reload                                              // after each change to build definition

Myriade de coquillages

scala              // Scala REPL that executes Scala language (nothing to do with sbt)
sbt                // sbt REPL that executes special sbt shell language (not Scala REPL)
sbt console        // Scala REPL with dependencies loaded as per build.sbt
sbt consoleProject // Scala REPL with project definition and sbt loaded for exploration with plain Scala langauage

La définition de construction est un projet Scala approprié

C'est l'un des concepts sbt idiomatiques clés. Je vais essayer d'expliquer avec une question. Supposons que vous souhaitiez définir une tâche sbt qui exécutera une requête HTTP avec scalaj-http. Intuitivement, nous pourrions essayer ce qui suit à l'intérieurbuild.sbt

libraryDependencies +=  "org.scalaj" %% "scalaj-http" % "2.4.2"

val fooTask = taskKey[Unit]("Fetch meaning of life")
fooTask := {
  import scalaj.http._ // error: cannot resolve symbol
  val response = Http("http://example.com").asString
  ...
}

Cependant, cela entraînera une erreur en disant qu'il manque import scalaj.http._. Comment est - ce possible quand nous, juste au- dessus, ajouté scalaj-httpà libraryDependencies? De plus, pourquoi cela fonctionne-t-il quand, à la place, nous ajoutons la dépendance project/build.sbt?

// project/build.sbt
libraryDependencies +=  "org.scalaj" %% "scalaj-http" % "2.4.2"

La réponse est que cela fooTaskfait en fait partie d'un projet Scala distinct de votre projet principal. Ce projet Scala différent se trouve dans le project/répertoire qui a son propre target/répertoire où résident ses classes compilées. En fait, sous project/target/config-classesil devrait y avoir une classe qui se décompile en quelque chose comme

object $9c2192aea3f1db3c251d extends scala.AnyRef {
  lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
  lazy val root : sbt.Project = { /* compiled code */ }
}

Nous voyons que fooTaskc'est simplement un membre d'un objet Scala régulier nommé $9c2192aea3f1db3c251d. De toute évidence, cela scalaj-httpdevrait être une dépendance de la définition du projet $9c2192aea3f1db3c251det non la dépendance du projet approprié. Par conséquent, il doit être déclaré dans project/build.sbtau lieu de build.sbt, car projectc'est là que réside le projet Scala de définition de construction.

Pour indiquer que la définition de construction n'est qu'un autre projet Scala, exécutez sbt consoleProject. Cela chargera Scala REPL avec le projet de définition de build sur le chemin de classe. Vous devriez voir une importation du type

import $9c2192aea3f1db3c251d

Nous pouvons donc maintenant interagir directement avec le projet de définition de construction en l'appelant avec Scala proprement dit au lieu de build.sbtDSL. Par exemple, ce qui suit exécutefooTask

$9c2192aea3f1db3c251d.fooTask.eval

build.sbtsous le projet racine est un DSL spécial qui aide à définir la définition de construction du projet Scala sous project/.

Et le projet Scala de définition de build, peut avoir son propre projet Scala de définition de build sous project/project/et ainsi de suite. Nous disons que sbt est récursif .

sbt est parallèle par défaut

sbt construit le DAG à partir des tâches. Cela lui permet d'analyser les dépendances entre les tâches et de les exécuter en parallèle et même d'effectuer la déduplication. build.sbtDSL est conçu dans cet esprit, ce qui pourrait conduire à une sémantique initialement surprenante. Selon vous, quel est l'ordre d'exécution dans l'extrait suivant?

def a = Def.task { println("a") }
def b = Def.task { println("b") }
lazy val c = taskKey[Unit]("sbt is parallel by-default")
c := {
  println("hello")
  a.value
  b.value
}

Intuitivement, on pourrait penser que le flux consiste d'abord à imprimer hellopuis à exécuter a, puis à effectuer une btâche. Cependant, cela signifie en fait exécuter aet ben parallèle , et avant println("hello") cela

a
b
hello

ou parce que l'ordre de aet bn'est pas garanti

b
a
hello

Peut-être paradoxalement, en sbt, il est plus facile de faire du parallèle que de la série. Si vous avez besoin d'une commande en série, vous devrez utiliser des choses spéciales comme Def.sequentialou Def.taskDynémuler pour la compréhension .

def a = Def.task { println("a") }
def b = Def.task { println("b") }
lazy val c = taskKey[Unit]("")
c := Def.sequential(
  Def.task(println("hello")),
  a,
  b
).value

est similaire à

for {
  h <- Future(println("hello"))
  a <- Future(println("a"))
  b <- Future(println("b"))
} yield ()

où nous voyons qu'il n'y a pas de dépendances entre les composants, tandis que

def a = Def.task { println("a"); 1 }
def b(v: Int) = Def.task { println("b"); v + 40 }
def sum(x: Int, y: Int) = Def.task[Int] { println("sum"); x + y }
lazy val c = taskKey[Int]("")
c := (Def.taskDyn {
  val x = a.value
  val y = Def.task(b(x).value)
  Def.taskDyn(sum(x, y.value))
}).value

est similaire à

def a = Future { println("a"); 1 }
def b(v: Int) = Future { println("b"); v + 40 }
def sum(x: Int, y: Int) = Future { x + y }

for {
  x <- a
  y <- b(x)
  c <- sum(x, y)
} yield { c }

où nous voyons sumdépend et doit attendre aet b.

En d'autres termes

  • pour la sémantique applicative , utilisez.value
  • pour une utilisation sémantique monadiquesequential outaskDyn

Considérez un autre extrait de code sémantiquement déroutant en raison de la nature de construction de dépendances de value, où au lieu de

`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
                ^

il faut écrire

val x = settingKey[String]("")
x := version.value

Notez que la syntaxe .valueconcerne les relations dans le DAG et ne signifie pas

"donne-moi la valeur maintenant"

au lieu de cela, cela signifie quelque chose comme

"mon interlocuteur dépend d'abord de moi, et une fois que je saurai comment l'ensemble du DAG s'emboîte, je serai en mesure de fournir à mon interlocuteur la valeur demandée"

Alors maintenant, il pourrait être un peu plus clair pourquoi xune valeur ne peut pas encore être attribuée; il n'y a pas encore de valeur disponible à l'étape de l'établissement de relations.

Nous pouvons clairement voir une différence de sémantique entre Scala proprement dit et le langage DSL dans build.sbt. Voici quelques règles de base qui fonctionnent pour moi

  • Le DAG est composé d'expressions de type Setting[T]
  • Dans la plupart des cas, nous utilisons simplement la .valuesyntaxe et sbt se chargera d'établir une relation entreSetting[T]
  • Parfois, nous devons modifier manuellement une partie du DAG et pour cela nous utilisons Def.sequentialouDef.taskDyn
  • Une fois que ces bizarreries syntatiques d'ordre / relation sont prises en compte, nous pouvons nous fier à la sémantique Scala habituelle pour construire le reste de la logique métier des tâches.

 Commandes vs tâches

Les commandes sont un moyen paresseux de sortir du DAG. En utilisant des commandes, il est facile de modifier l'état de construction et de sérialiser les tâches comme vous le souhaitez. Le coût est que nous perdons la parallélisation et la déduplication des tâches fournies par DAG, ce qui devrait être le choix préféré des tâches. Vous pouvez considérer les commandes comme une sorte d'enregistrement permanent d'une session que l'on pourrait faire à l'intérieur sbt shell. Par exemple, étant donné

vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value

considérez le résultat de la session suivante

sbt:root> x
[info] 13
sbt:root> show f
[info] 14
sbt:root> set x := 41
[info] Defining x
[info] The new value will be used by f
[info] Reapplying settings...
sbt:root> show f
[info] 42

En particulier, pas comment nous modifions l'état de construction avec set x := 41. Les commandes nous permettent de faire un enregistrement permanent de la session ci-dessus, par exemple

commands += Command.command("cmd") { state =>
  "x" :: "show f" :: "set x := 41" :: "show f" :: state
}

Nous pouvons également rendre la commande sécurisée en utilisant Project.extractetrunTask

commands += Command.command("cmd") { state =>
  val log = state.log
  import Project._
  log.info(x.value.toString)
  val (_, resultBefore) = extract(state).runTask(f, state)
  log.info(resultBefore.toString)
  val mutatedState = extract(state).appendWithSession(Seq(x := 41), state)
  val (_, resultAfter) = extract(mutatedState).runTask(f, mutatedState)
  log.info(resultAfter.toString)
  mutatedState
}

Portées

Les champs d'application entrent en jeu lorsque nous essayons de répondre aux types de questions suivants

  • Comment définir une tâche une fois et la rendre disponible à tous les sous-projets en construction multi-projets?
  • Comment éviter d'avoir des dépendances de test sur le chemin de classe principal?

sbt a un espace de portée multi-axes qui peut être parcouru à l'aide de la syntaxe slash , par exemple,

show  root   /  Compile         /  compile   /   scalacOptions
        |        |                  |             |
     project    configuration      task          key

Personnellement, je me retrouve rarement à me soucier de la portée. Parfois, je veux compiler uniquement des sources de test

Test/compile

ou peut-être exécuter une tâche particulière à partir d'un sous-projet particulier sans avoir à naviguer vers ce projet avec project subprojB

subprojB/Test/compile

Je pense que les règles empiriques suivantes aident à éviter les complications de portée

  • n'ont pas plusieurs build.sbtfichiers mais seulement un seul maître sous le projet racine qui contrôle tous les autres sous-projets
  • partager des tâches via des plugins automatiques
  • factoriser les paramètres communs dans Scala simple valet l'ajouter explicitement à chaque sous-projet

Construction multi-projets

Au lieu de plusieurs fichiers build.sbt pour chaque sous-projet

.
├── README.md
├── build.sbt                  // OK
├── multi1
│   ├── build.sbt              // NOK
│   ├── src
│   └── target
├── multi2
│   ├── build.sbt              // NOK
│   ├── src
│   └── target
├── project                    // this is the meta-project
│   ├── FooPlugin.scala        // custom auto plugin
│   ├── build.properties       // version of sbt and hence Scala for meta-project
│   ├── build.sbt              // OK - this is actually for meta-project 
│   ├── plugins.sbt            // OK
│   ├── project
│   └── target
└── target

Avoir un seul maître build.sbtpour les gouverner tous

.
├── README.md
├── build.sbt                  // single build.sbt to rule theme all
├── common
│   ├── src
│   └── target
├── multi1
│   ├── src
│   └── target
├── multi2
│   ├── src
│   └── target
├── project
│   ├── FooPlugin.scala
│   ├── build.properties
│   ├── build.sbt
│   ├── plugins.sbt
│   ├── project
│   └── target
└── target

Il existe une pratique courante de prise en compte des paramètres communs dans les versions multi-projets

définir une séquence de paramètres communs dans un val et les ajouter à chaque projet. Moins de concepts à apprendre de cette façon.

par exemple

lazy val commonSettings = Seq(
  scalacOptions := Seq(
    "-Xfatal-warnings",
    ...
  ),
  publishArtifact := true,
  ...
)

lazy val root = project
  .in(file("."))
  .settings(settings)
  .aggregate(
    multi1,
    multi2
  )
lazy val multi1 = (project in file("multi1")).settings(commonSettings)
lazy val multi2 = (project in file("multi2")).settings(commonSettings)

Navigation des projets

projects         // list all projects
project multi1   // change to particular project

Plugins

N'oubliez pas que la définition de construction est un projet Scala approprié qui réside sous project/. C'est ici que nous définissons un plugin en créant des .scalafichiers

.                          // directory of the (main) proper project
├── project
│   ├── FooPlugin.scala    // auto plugin
│   ├── build.properties   // version of sbt library and indirectly Scala used for the plugin
│   ├── build.sbt          // build definition of the plugin
│   ├── plugins.sbt        // these are plugins for the main (proper) project, not the meta project
│   ├── project            // the turtle supporting this turtle
│   └── target             // compiled binaries of the plugin

Voici un plugin automatique minimal sousproject/FooPlugin.scala

object FooPlugin extends AutoPlugin {
  object autoImport {
      val barTask = taskKey[Unit]("")
  }

  import autoImport._

  override def requires = plugins.JvmPlugin  // avoids having to call enablePlugin explicitly
  override def trigger = allRequirements

  override lazy val projectSettings = Seq(
    scalacOptions ++= Seq("-Xfatal-warnings"),
    barTask := { println("hello task") },
    commands += Command.command("cmd") { state =>
      """eval println("hello command")""" :: state
    }   
  )
}

Le remplacement

override def requires = plugins.JvmPlugin

devrait permettre efficacement le plug - in pour tous les sous-projets sans avoir à appeler explicitement enablePlugindans build.sbt.

IntelliJ et sbt

Veuillez activer le paramètre suivant (qui devrait vraiment être activé par défaut )

use sbt shell

sous

Preferences | Build, Execution, Deployment | sbt | sbt projects

Références clés

Mario Galic
la source