Qualcuno può spiegare il modo giusto per usare SBT?


100

Sto uscendo dall'armadio su questo! Non capisco SBT. Ecco, l'ho detto, ora aiutami per favore.

Tutte le strade portano a Roma, e che è lo stesso per SBT: Per iniziare SBTc'è SBT, SBT Launcher, SBT-extras, ecc, e poi ci sono diversi modi per includere e decidere sul repository. Esiste un modo "migliore"?

Te lo chiedo perché a volte mi perdo un po '. La documentazione SBT è molto approfondita e completa, ma mi ritrovo a non sapere quando usare build.sbto project/build.propertieso project/Build.scalao project/plugins.sbt.

Poi diventa divertente, c'è il Scala-IDEe SBT- Qual è il modo corretto di usarli insieme? Cosa viene prima, la gallina o l'uovo?

La cosa più importante è probabilmente, come trovi i repository e le versioni giusti da includere nel tuo progetto? Devo semplicemente tirare fuori un machette e iniziare a fare hacking per andare avanti? Trovo spesso progetti che includono tutto e il lavello della cucina, e poi mi rendo conto che non sono l'unico che si perde un po '.

A titolo di semplice esempio, in questo momento, sto iniziando un nuovo progetto. Voglio usare le ultime funzionalità di SLICKe Scalae questo probabilmente richiederà l'ultima versione di SBT. Qual è il punto giusto per iniziare e perché? In quale file dovrei definirlo e come dovrebbe apparire? So che posso farlo funzionare, ma mi piacerebbe davvero un'opinione di esperti su dove dovrebbe andare tutto (perché dovrebbe andare ci sarà un bonus).

Lo uso SBTper piccoli progetti da oltre un anno. Ho usato SBTe poi SBT Extras(poiché ha fatto sparire magicamente alcuni mal di testa), ma non sono sicuro del motivo per cui dovrei usare l'uno o l'altro. Sto solo diventando un po 'frustrato per non aver capito come le cose si adattano ( SBTe gli archivi), e penso che salverà il prossimo ragazzo che viene da questa parte di molte difficoltà se questo potesse essere spiegato in termini umani.


2
Cosa intendi esattamente con "ci sono Scala-IDE e SBT"? Definisci il tuo progetto con sbt e sbt può generare un progetto ide (eclipse oder intellij). Così SBT viene prima ...
Jan

2
@ Jan l'ho detto perché Scala-IDE usa SBT come build manager. Vedi assembla.com/spaces/scala-ide/wiki/SBT-based_build_manager e più in basso nel post che menzionano "Non è necessario definire il tuo file di progetto SBT." che ho trovato confuso.
Jack

ok. Dato che di solito uso intellij (o sublime) per modificare scala, non lo sapevo. Immagino che il costruttore generi le proprie configurazioni sbt?
gennaio

2
@JacobusR Il fatto che l'IDE Scala utilizzi SBT per costruire i sorgenti del tuo progetto è un dettaglio di implementazione e gli utenti non devono preoccuparsi di questo. Ci sono davvero 0 implicazioni. Al di fuori di Eclipse gli utenti possono costruire un progetto con SBT, Maven, Ant, ... e questo non farà alcuna differenza per l'IDE Scala. Un'altra cosa, anche se hai un progetto SBT, l'IDE Scala non si preoccupa, cioè non cerca il tuo Build.scalaper impostare il classpath, ed è per questo che hai effettivamente bisogno di sbteclipse per generare il .classpath di Eclipse. Spero che questo ti aiuti.
Mirco Dotta

1
@Jan Scala IDE ha aggiunto alla confusione, e sì, la documentazione che fornisce un quadro più ampio sulla creazione di un buon ambiente di sviluppo Scala e alcune solide indicazioni per flussi di lavoro di programmazione adeguati sarebbero molto utili.
Jack

Risposte:


29

La cosa più importante è probabilmente, come trovi i repository e le versioni giusti da includere nel tuo progetto? Devo semplicemente tirare fuori un machette e iniziare a fare hacking per andare avanti? Trovo abbastanza spesso progetti che includono tutto e il lavello della cucina

Per le dipendenze basate su Scala, sceglierei ciò che gli autori raccomandano. Ad esempio: http://code.google.com/p/scalaz/#SBT indica di utilizzare:

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

Oppure https://github.com/typesafehub/sbteclipse/ contiene istruzioni su dove aggiungere:

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

Per le dipendenze basate su Java, utilizzo http://mvnrepository.com/ per vedere cosa c'è là fuori, quindi faccio clic sulla scheda SBT. Ad esempio http://mvnrepository.com/artifact/net.sf.opencsv/opencsv/2.3 indica di utilizzare:

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

Quindi tira fuori la machette e inizia a farti strada. Se sei fortunato non finisci per usare barattoli che dipendono da alcuni degli stessi barattoli ma con versioni incompatibili. Dato l'ecosistema Java, spesso finisci per includere tutto e il lavello della cucina e ci vuole un po 'di sforzo per eliminare le dipendenze o assicurarti di non perdere le dipendenze richieste.

A titolo di semplice esempio, in questo momento, sto iniziando un nuovo progetto. Voglio usare le ultime funzionalità di SLICK e Scala e questo probabilmente richiederà l'ultima versione di SBT. Qual è il punto giusto per iniziare e perché?

Penso che il punto sano sia costruire gradualmente l'immunità a SBT .

Assicurati di aver compreso:

  1. formato ambiti {<build-uri>}<project-id>/config:key(for task-key)
  2. i 3 gusti di impostazioni ( SettingKey, TaskKey, InputKey) - leggere la sezione chiamata "chiavi di operazione" in http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def

Tieni sempre aperte quelle 4 pagine in modo da poter saltare e cercare varie definizioni ed esempi:

  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

Sfrutta al massimo showe inspect e il completamento con tabulazione per acquisire familiarità con i valori effettivi delle impostazioni, le loro dipendenze, definizioni e impostazioni correlate. Non credo che le relazioni che scoprirai usando inspectsiano documentate da nessuna parte. Se c'è un modo migliore, voglio saperlo.


25

Il modo in cui uso sbt è:

  1. Usa sbt-extras : prendi lo script della shell e aggiungilo alla radice del tuo progetto
  2. Crea una projectcartella con un MyProject.scalafile per configurare sbt. Lo preferisco di gran lunga build.sbtall'approccio: è in scala ed è più flessibile
  3. Crea un project/plugins.sbtfile e aggiungi il plugin appropriato per il tuo IDE. O sbt-eclipse, sbt-idea o ensime-sbt-cmd in modo da poter generare file di progetto per eclipse, intellij o ensime.
  4. Avvia sbt nella radice del tuo progetto e genera i file di progetto per il tuo IDE
  5. Profitto

Non mi preoccupo di controllare i file di progetto IDE poiché sono generati da sbt, ma potrebbero esserci ragioni per cui vuoi farlo.

Puoi vedere un esempio impostato come questo qui .


Grazie per la buona risposta. Ho accettato l'altra risposta, perché copre più terreno, e ho votato al rialzo la tua causa è anche molto buona. Avrei accettato entrambi se avessi potuto.
Jack

0

Usa Typesafe Activator, un modo elegante per chiamare sbt, che viene fornito con modelli di progetto e seed: 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)

5
Sono parziale all'idea che, in caso di dubbio, l'aggiunta di più magia al mix non risolverà i tuoi problemi.
cubo

0

Installazione

brew install sbt o simili installa sbt che tecnicamente parlando consiste

Quando esegui sbtdal terminale, esegue effettivamente lo script bash del lanciatore sbt. Personalmente, non mi sono mai dovuto preoccupare di questa trinità, e ho usato semplicemente sbt come se fosse una cosa sola.

Configurazione

Per configurare sbt per un particolare progetto, salvare il .sbtoptsfile nella radice del progetto. Per configurare sbt modificare a livello di sistema /usr/local/etc/sbtopts. L'esecuzione sbt -helpdovrebbe dirti la posizione esatta. Ad esempio, per dare a sbt più memoria durante l'esecuzione una tantum sbt -mem 4096, o salvare -mem 4096in .sbtoptso sbtoptsaffinché l'aumento di memoria abbia effetto in modo permanente.

 Struttura del progetto

sbt new scala/scala-seed.g8 crea una struttura minima del progetto Hello World sbt

.
├── 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

Comandi frequenti

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

Una miriade di conchiglie

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 definizione della build è un vero e proprio progetto Scala

Questo è uno dei concetti chiave di sbt idiomatici. Proverò a spiegare con una domanda. Supponiamo che tu voglia definire un'attività sbt che eseguirà una richiesta HTTP con scalaj-http. Intuitivamente potremmo provare quanto segue all'internobuild.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
  ...
}

Tuttavia questo errore sarà detto mancante import scalaj.http._. Come è possibile quando, proprio sopra, aggiunto scalaj-httpa libraryDependencies? Inoltre, perché funziona quando, invece, aggiungiamo la dipendenza a project/build.sbt?

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

La risposta è che in fooTaskrealtà fa parte di un progetto Scala separato dal tuo progetto principale. Questo diverso progetto Scala può essere trovato nella project/directory che ha la sua target/directory in cui risiedono le classi compilate. In effetti, sotto project/target/config-classesdovrebbe esserci una classe che si decompila in qualcosa di simile

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

Vediamo che fooTaskè semplicemente un membro di un normale oggetto Scala denominato $9c2192aea3f1db3c251d. Chiaramente scalaj-httpdovrebbe essere una dipendenza della definizione del progetto $9c2192aea3f1db3c251de non la dipendenza del progetto corretto. Quindi deve essere dichiarato in project/build.sbtinvece di build.sbt, perché projectè dove risiede la definizione di build del progetto Scala.

Per guidare il punto che la definizione di build è solo un altro progetto Scala, esegui sbt consoleProject. Questo caricherà Scala REPL con il progetto di definizione build sul classpath. Dovresti vedere un'importazione lungo le linee di

import $9c2192aea3f1db3c251d

Quindi ora possiamo interagire direttamente con il progetto di definizione build chiamandolo con Scala vera e propria invece di build.sbtDSL. Ad esempio, viene eseguito quanto seguefooTask

$9c2192aea3f1db3c251d.fooTask.eval

build.sbtunder root project è un DSL speciale che aiuta a definire la definizione di build del progetto Scala in project/.

E la definizione di build del progetto Scala, può avere la propria definizione di build del progetto Scala in project/project/e così via. Diciamo che sbt è ricorsivo .

sbt è parallelo per impostazione predefinita

sbt compila il DAG dalle attività. Ciò gli consente di analizzare le dipendenze tra le attività e di eseguirle in parallelo e persino di eseguire la deduplicazione. build.sbtDSL è progettato tenendo presente questo aspetto, il che potrebbe portare a una semantica inizialmente sorprendente. Quale pensi che sia l'ordine di esecuzione nel seguente frammento?

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
}

Intuitivamente si potrebbe pensare che il flusso qui sia prima di stampare, helloquindi eseguire ae quindi eseguire l' battività. Tuttavia questo in realtà significa eseguire ae bin parallelo , e prima println("hello") ancora

a
b
hello

o perché l'ordine di ae bnon è garantito

b
a
hello

Forse paradossalmente, in sbt è più facile fare parallelo che seriale. Se hai bisogno di un ordine seriale, dovrai usare cose speciali come Def.sequentialo Def.taskDynper emulare per la comprensione .

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

è simile a

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

dove vediamo che non ci sono dipendenze tra i componenti, mentre

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

è simile a

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 }

dove vediamo sumdipende e deve aspettare ae b.

In altre parole

  • per la semantica applicativa , utilizzare.value
  • per la semantica monadica utilizzare sequentialotaskDyn

Considera un altro frammento semanticamente confuso come risultato della natura di costruzione delle dipendenze di value, dove invece di

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

dobbiamo scrivere

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

Nota che la sintassi .valueriguarda le relazioni nel DAG e non significa

"dammi il valore adesso"

invece significa qualcosa di simile

"il mio chiamante dipende innanzitutto da me e, una volta che so come si integra l'intero DAG, sarò in grado di fornire al mio chiamante il valore richiesto"

Quindi ora potrebbe essere un po 'più chiaro il motivo per cui xnon è ancora possibile assegnare un valore; non è ancora disponibile alcun valore nella fase di costruzione della relazione.

Possiamo chiaramente vedere una differenza nella semantica tra Scala vera e propria e il linguaggio DSL in formato build.sbt. Ecco alcune regole pratiche che funzionano per me

  • DAG è composto da espressioni di tipo Setting[T]
  • Nella maggior parte dei casi usiamo semplicemente la .valuesintassi e sbt si occuperà di stabilire una relazione traSetting[T]
  • Occasionalmente dobbiamo modificare manualmente una parte di DAG e per questo utilizziamo Def.sequentialoDef.taskDyn
  • Una volta risolte queste stranezze sintatiche di ordinamento / relazione, possiamo fare affidamento sulla solita semantica Scala per costruire il resto della logica di business dei compiti.

 Comandi vs compiti

I comandi sono una via d'uscita pigra dal DAG. Utilizzando i comandi è facile modificare lo stato di compilazione e serializzare le attività come desideri. Il costo è che perdiamo la parallelizzazione e la deduplicazione delle attività fornite da DAG, in questo modo le attività dovrebbero essere la scelta preferita. Puoi pensare ai comandi come a una sorta di registrazione permanente di una sessione che si potrebbe fare all'interno sbt shell. Ad esempio, dato

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

considera l'output della sessione successiva

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

In particolare, non il modo in cui mutiamo lo stato di compilazione con set x := 41. I comandi ci consentono, ad esempio, di effettuare una registrazione permanente della sessione precedente

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

Possiamo anche rendere il comando indipendente dai tipi usando Project.extracterunTask

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
}

Ambiti

Gli ambiti entrano in gioco quando proviamo a rispondere ai seguenti tipi di domande

  • Come definire l'attività una volta e renderla disponibile a tutti i sottoprogetti nella build multi-progetto?
  • Come evitare di avere dipendenze di test sul classpath principale?

sbt ha uno spazio di scoping multiasse che può essere esplorato utilizzando la sintassi della barra , ad esempio,

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

Personalmente, raramente mi trovo a dovermi preoccupare dell'ambito. A volte voglio compilare solo sorgenti di test

Test/compile

o forse eseguire una particolare attività da un particolare sottoprogetto senza dover prima passare a quel progetto con project subprojB

subprojB/Test/compile

Penso che le seguenti regole pratiche aiutano a evitare complicazioni di scoping

  • non hanno più build.sbtfile ma solo un unico master sotto il progetto principale che controlla tutti gli altri sottoprogetti
  • condividere attività tramite plug-in automatici
  • scomporre le impostazioni comuni in Scala semplice vale aggiungerla esplicitamente a ogni sottoprogetto

Build multiprogetto

Invece di più file build.sbt per ogni sottoprogetto

.
├── 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

Avere un unico maestro build.sbtper governarli tutti

.
├── 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

Esiste una pratica comune di fattorizzare le impostazioni comuni nelle build multi-progetto

definire una sequenza di impostazioni comuni in una val e aggiungerle a ciascun progetto. Meno concetti da imparare in questo modo.

per esempio

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)

Navigazione progetti

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

Plugin

Ricorda che la definizione di build è un vero e proprio progetto Scala che risiede sotto project/. È qui che definiamo un plugin creando .scalafile

.                          // 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

Ecco un plug-in automatico minimo inproject/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
    }   
  )
}

L'override

override def requires = plugins.JvmPlugin

dovrebbe abilitare efficacemente il plugin per tutti i sottoprogetti senza dover chiamare esplicitamente enablePluginin build.sbt.

IntelliJ e sbt

Abilita la seguente impostazione (che dovrebbe essere abilitata per impostazione predefinita )

use sbt shell

sotto

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

Riferimenti chiave

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.