Czy ktoś może wyjaśnić właściwy sposób korzystania z SBT?

100

Wychodzę z szafy w tej sprawie! Nie rozumiem SBT. Tam, powiedziałem to, teraz pomóż mi, proszę.

Wszystkie drogi prowadzą do Rzymu, a to jest taki sam dla SBT: Aby zacząć SBTtam jest SBT, SBT Launcher, SBT-extrasitp, a następnie istnieją różne sposoby obejmują i zdecydować się na repozytoriach. Czy istnieje „najlepszy” sposób?

Pytam, bo czasem się trochę gubię. Dokumentacja SBT jest bardzo dokładna i kompletna, ale nie wiem, kiedy użyć build.sbtlub project/build.propertieslub project/Build.scalalub project/plugins.sbt.

Wtedy staje się zabawne, jest Scala-IDEi SBT- Jaki jest prawidłowy sposób ich wspólnego używania? Co jest pierwsze, kura czy jajko?

Najważniejsze jest prawdopodobnie, jak znaleźć odpowiednie repozytoria i wersje, które chcesz uwzględnić w swoim projekcie? Czy po prostu wyciągam maczetę i zaczynam się przedzierać? Dość często znajduję projekty, które zawierają wszystko i zlewozmywak i wtedy zdaję sobie sprawę - nie tylko ja się trochę gubię.

Jako prosty przykład, zaczynam teraz zupełnie nowy projekt. Chcę korzystać z najnowszych funkcji SLICKi Scalai to prawdopodobnie wymagać będzie ostatnią wersję SBT. Jaki jest rozsądny punkt na początek i dlaczego? W jakim pliku mam to zdefiniować i jak powinien wyglądać? Wiem, że mogę to uruchomić, ale naprawdę chciałbym uzyskać opinię eksperta na temat tego, gdzie wszystko powinno się udać (dlaczego powinno iść, będzie premia).

Używam SBTdo małych projektów od ponad roku. Użyłem, SBTa potem SBT Extras(ponieważ spowodowało to magiczne zniknięcie niektórych bólów głowy), ale nie jestem pewien, dlaczego powinienem używać jednego lub drugiego. Jestem trochę sfrustrowany, że nie rozumiem, jak rzeczy do siebie pasują ( SBTi repozytoria) i myślę, że zaoszczędzi to następnemu facetowi, który przychodzi w ten sposób, wiele trudności, jeśli można to wyjaśnić w kategoriach ludzkich.

Jacek
źródło
2
Co dokładnie masz na myśli mówiąc „jest Scala-IDE i SBT”? Definiujesz swój projekt za pomocą sbt, a sbt może wygenerować projekt ide (eclipse oder intellij). Więc SBT jest na pierwszym miejscu ...
Sty
2
@Jan Wspomniałem o tym, ponieważ Scala-IDE używa SBT jako menedżera kompilacji. Zobacz assembla.com/spaces/scala-ide/wiki/SBT-based_build_manager i niżej w poście, w którym wspominają „Nie ma potrzeby definiowania pliku projektu SBT”. co wydało mi się mylące.
Jack,
dobrze. Ponieważ zwykle używam intellij (lub sublime) do edycji scali, nie wiedziałem tego. Domyślam się, że konstruktor generuje własne konfiguracje sbt?
Sty
2
@JacobusR Fakt, że Scala IDE używa SBT do budowania źródeł twojego projektu, jest szczegółem implementacji i użytkownicy nie muszą się tym martwić. Naprawdę jest 0 implikacji. Użytkownicy spoza Eclipse mogą budować projekty za pomocą SBT, Maven, Ant, ... i nie będzie to miało żadnego znaczenia dla Scala IDE. I jeszcze jedno, nawet jeśli masz projekt SBT, Scala IDE nie dba o to, tj. Nie szuka twojej Build.scalaścieżki klas i dlatego tak naprawdę potrzebujesz sbteclipse, aby wygenerować Eclipse .classpath. Mam nadzieję że to pomoże.
Mirco Dotta,
1
@Jan Scala IDE dodało zamieszania i tak, dokumentacja, która daje większy obraz konfigurowania dobrego środowiska programistycznego Scala i pewne solidne wskazówki dotyczące odpowiednich przepływów pracy programowania, byłaby bardzo przydatna.
Jack,

Odpowiedzi:

29

Najważniejsze jest prawdopodobnie, jak znaleźć odpowiednie repozytoria i wersje, które chcesz uwzględnić w swoim projekcie? Czy po prostu wyciągam maczetę i zaczynam się przedzierać? Dość często znajduję projekty zawierające wszystko i zlewozmywak

W przypadku zależności opartych na Scali postąpiłbym zgodnie z zaleceniami autorów. Na przykład: http://code.google.com/p/scalaz/#SBT wskazuje na użycie:

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

Lub https://github.com/typesafehub/sbteclipse/ zawiera instrukcje, gdzie dodać:

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

W przypadku zależności opartych na Javie używam http://mvnrepository.com/, aby zobaczyć, co tam jest, a następnie klikam kartę SBT. Na przykład http://mvnrepository.com/artifact/net.sf.opencsv/opencsv/2.3 wskazuje na użycie:

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

Następnie wyciągnij maczetę i zacznij siekać przed siebie. Jeśli masz szczęście, nie będziesz używać słoików, które zależą od niektórych z tych samych słoików, ale z niekompatybilnymi wersjami. Biorąc pod uwagę ekosystem Java, często kończy się to na dołączeniu wszystkiego i zlewu kuchennego, a wyeliminowanie zależności lub zapewnienie, że nie brakuje wymaganych zależności, wymaga pewnego wysiłku.

Jako prosty przykład, zaczynam teraz zupełnie nowy projekt. Chcę korzystać z najnowszych funkcji SLICK i Scala, a to prawdopodobnie będzie wymagało najnowszej wersji SBT. Jaki jest rozsądny punkt na początek i dlaczego?

Myślę, że rozsądnym punktem jest stopniowe budowanie odporności na kogoś .

Upewnij się, że rozumiesz:

  1. format zakresów {<build-uri>}<project-id>/config:key(for task-key)
  2. W 3 smaki ustawieniami ( SettingKey, TaskKey, InputKey) - przeczytaj rozdział o nazwie „Keys zadania” w http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def

Miej te 4 strony otwarte przez cały czas, abyś mógł przeskoczyć i wyszukać różne definicje i przykłady:

  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

Maksymalnie wykorzystaj showi inspect oraz uzupełnianie kart, aby zapoznać się z rzeczywistymi wartościami ustawień, ich zależnościami, definicjami i powiązanymi ustawieniami. Nie wierzę, że relacje, które odkryjesz, inspectsą gdziekolwiek udokumentowane. Jeśli istnieje lepszy sposób, chcę się o tym dowiedzieć.

huynhjl
źródło
25

Sposób, w jaki używam sbt, to:

  1. Użyj sbt-extras - po prostu pobierz skrypt powłoki i dodaj go do katalogu głównego swojego projektu
  2. Utwórz projectfolder z MyProject.scalaplikiem do konfiguracji sbt. O wiele wolę to od build.sbtpodejścia - jest skala i jest bardziej elastyczne
  3. Utwórz project/plugins.sbtplik i dodaj odpowiednią wtyczkę do swojego IDE. Albo sbt-eclipse, sbt-idea lub ensime-sbt-cmd, aby można było generować pliki projektów dla eclipse, intellij lub ensime.
  4. Uruchom sbt w katalogu głównym projektu i wygeneruj pliki projektu dla swojego IDE
  5. Zysk

Nie zawracam sobie głowy sprawdzaniem plików projektu IDE, ponieważ są one generowane przez sbt, ale mogą być powody, dla których chcesz to zrobić.

Możesz zobaczyć przykład skonfigurowany w ten sposób tutaj .

Channing Walton
źródło
Dziękuję za dobrą odpowiedź. Zaakceptowałem drugą odpowiedź, ponieważ obejmuje ona więcej powodów i pozytywnie głosowała za twoją, ponieważ jest również naprawdę dobra. Przyjąłbym oba, gdybym mógł.
Jack,
0

Użyj Aktywatora Typesafe, fantazyjnego sposobu dzwonienia do sbt, który jest dostarczany z szablonami projektów i nasionami: 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
źródło
5
Jestem stronniczy w przekonaniu, że w razie wątpliwości dodanie większej ilości magii do miksu prawdopodobnie nie rozwiąże twoich problemów.
Sześcienny
0

Instalacja

brew install sbt lub podobne instalacje, z których technicznie rzecz biorąc składa się

Kiedy wykonujesz sbtz terminala, faktycznie uruchamia skrypt bash programu uruchamiającego SBT. Osobiście nigdy nie musiałem martwić się o tę trójcę i po prostu używać sbt tak, jakby to była jedna rzecz.

Konfiguracja

Aby skonfigurować sbt dla określonego .sbtoptspliku zapisu projektu w katalogu głównym projektu. Aby skonfigurować sbt w całym systemie, modyfikuj /usr/local/etc/sbtopts. Wykonanie sbt -helppowinno podać dokładną lokalizację. Na przykład, aby dać sbt więcej pamięci jako jednorazowe wykonanie sbt -mem 4096lub zapisać -mem 4096w .sbtoptslub w sbtoptscelu zwiększenia pamięci, aby zadziałało na stałe.

 Struktura projektu

sbt new scala/scala-seed.g8 tworzy minimalną strukturę projektu 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

Częste polecenia

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

Niezliczone muszle

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

Definicja kompilacji to właściwy projekt Scala

To jedna z kluczowych idiomatycznych koncepcji sbt. Spróbuję wyjaśnić pytaniem. Powiedzmy, że chcesz zdefiniować zadanie SBT, które wykona żądanie HTTP za pomocą scalaj-http. Intuicyjnie możemy wypróbować następujące rozwiązaniabuild.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
  ...
}

Jednak będzie to błąd z informacją o braku import scalaj.http._. Jak to możliwe, kiedy, tuż powyżej, dodaje scalaj-httpsię libraryDependencies? Co więcej, dlaczego to działa, skoro zamiast tego dodajemy zależność do project/build.sbt?

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

Odpowiedź jest taka, że ​​w fooTaskrzeczywistości jest to część oddzielnego projektu Scala od twojego głównego projektu. Ten inny projekt Scala można znaleźć w project/katalogu, który ma własny target/katalog, w którym znajdują się jego skompilowane klasy. W rzeczywistości pod project/target/config-classespowinna znajdować się klasa, która dekompiluje się do czegoś podobnego

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

Widzimy, że fooTaskjest to po prostu członek zwykłego obiektu Scala o nazwie $9c2192aea3f1db3c251d. Oczywiście scalaj-httppowinna to być zależność projektu definiującego, $9c2192aea3f1db3c251da nie zależność odpowiedniego projektu. Dlatego należy go zadeklarować w project/build.sbtzamiast build.sbt, ponieważ projectjest to miejsce, w którym znajduje się definicja kompilacji projekt Scala.

Aby wskazać, że definicja kompilacji to tylko kolejny projekt Scali, wykonaj sbt consoleProject. Spowoduje to załadowanie Scala REPL z projektem definicji kompilacji w ścieżce klas. Powinieneś zobaczyć import wzdłuż linii

import $9c2192aea3f1db3c251d

Więc teraz możemy bezpośrednio współdziałać z projektem definicji kompilacji, wywołując go za pomocą właściwej Scali zamiast build.sbtDSL. Na przykład wykonuje następujące czynnościfooTask

$9c2192aea3f1db3c251d.fooTask.eval

build.sbtw ramach projektu głównego jest specjalnym DSL, który pomaga zdefiniować definicję kompilacji projektu Scala w ramach project/.

Definicja kompilacji Projekt Scala może mieć własną definicję kompilacji Projekt Scala project/project/i tak dalej. Mówimy, że sbt jest rekurencyjny .

sbt jest domyślnie równoległy

sbt buduje DAG z zadań. Pozwala to analizować zależności między zadaniami i wykonywać je równolegle, a nawet przeprowadzać deduplikację. build.sbtDSL został zaprojektowany z myślą o tym, co może prowadzić do początkowo zaskakującej semantyki. Jak myślisz, jaka jest kolejność wykonywania w poniższym fragmencie?

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
}

Intuicyjnie można by pomyśleć płynąć tutaj jest do pierwszego wydruku hellopotem wykonanie a, a następnie bzadania. Jednak w rzeczywistości oznacza to wykonać ai bw równolegle , a przed println("hello") tak

a
b
hello

lub ponieważ kolejność ai bnie jest gwarantowana

b
a
hello

Być może paradoksalnie, w sbt łatwiej jest robić równolegle niż szeregowo. Jeśli potrzebujesz zamówień seryjnych, będziesz musiał użyć specjalnych rzeczy, takich jak Def.sequentiallub Def.taskDyndo naśladowania zrozumienia .

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

jest podobne do

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

gdzie widzimy, że nie ma zależności między komponentami, podczas gdy

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

jest podobne do

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 }

to, gdzie widzimy, sumzależy od i musi czekać na ai b.

Innymi słowy

  • dla semantyki aplikacyjnej użyj.value
  • dla semantyki monadycznej użyj sequentiallubtaskDyn

Rozważ inny semantycznie mylący fragment kodu w wyniku natury budowania zależności value, gdzie zamiast

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

musimy pisać

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

Zwróć uwagę, że składnia .valuedotyczy relacji w DAG i nie oznacza

„podaj mi teraz wartość”

zamiast tego oznacza coś takiego

„mój rozmówca zależy najpierw ode mnie, a kiedy już wiem, jak pasuje do siebie cały DAG, będę w stanie podać mojemu rozmówcy żądaną wartość”

Więc teraz może być trochę jaśniejsze, dlaczego xnie można jeszcze przypisać wartości; nie ma jeszcze wartości na etapie budowania relacji.

Wyraźnie widać różnicę w semantyce między właściwą Scalą a językiem DSL w build.sbt. Oto kilka praktycznych zasad, które sprawdzają się w moim przypadku

  • DAG składa się z wyrażeń typu Setting[T]
  • W większości przypadków po prostu używamy .valueskładni, a sbt zajmie się ustanowieniem relacji międzySetting[T]
  • Czasami musimy ręcznie zmodyfikować część DAG i do tego używamy Def.sequentiallubDef.taskDyn
  • Gdy już uporamy się z tymi osobliwościami w składni porządkowania / relacji, możemy polegać na zwykłej semantyce Scali do budowania pozostałej logiki biznesowej zadań.

 Polecenia a zadania

Polecenia są leniwym wyjściem z DAG. Za pomocą poleceń można łatwo modyfikować stan kompilacji i serializować zadania zgodnie z potrzebami. Kosztem jest to, że tracimy zrównoleglanie i deduplikację zadań dostarczanych przez DAG, którą drogą zadania powinny być preferowane. Możesz myśleć o poleceniach jako o rodzaju trwałego nagrania sesji, którą można wykonać w środku sbt shell. Na przykład podane

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

rozważ wyniki następnej sesji

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

W szczególności nie dotyczy to sposobu zmiany stanu kompilacji za pomocą set x := 41. Polecenia pozwalają np. Na trwałe nagranie powyższej sesji

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

Możemy również uczynić polecenie bezpiecznym, używając Project.extractirunTask

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
}

Zakresy

Lunety wchodzą w grę, gdy próbujemy odpowiedzieć na następujące rodzaje pytań

  • Jak raz zdefiniować zadanie i udostępnić je wszystkim podprojektom w kompilacji wieloprojektowej?
  • Jak uniknąć zależności testowych w głównej ścieżce klas?

sbt ma wieloosiową przestrzeń zakresu, po której można nawigować za pomocą składni ukośnika , na przykład

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

Osobiście rzadko muszę się martwić o zakres. Czasami chcę skompilować tylko źródła testowe

Test/compile

a może wykonać określone zadanie z określonego podprojektu bez konieczności wcześniejszego przechodzenia do tego projektu za pomocą project subprojB

subprojB/Test/compile

Myślę, że poniższe praktyczne zasady pomogą uniknąć komplikacji związanych z określaniem zakresu

  • nie mają wielu build.sbtplików, ale tylko jeden główny w projekcie głównym, który kontroluje wszystkie inne podprojekty
  • udostępniać zadania za pomocą wtyczek automatycznych
  • wyodrębnij typowe ustawienia do zwykłej Scali vali dodaj je do każdego podprojektu

Kompilacja wieloprojektowa

Zamiast wielu plików build.sbt dla każdego podprojektu

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

Miej jednego pana, build.sbtktóry będzie rządził wszystkimi

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

Istnieje powszechna praktyka uwzględniania wspólnych ustawień w kompilacjach obejmujących wiele projektów

zdefiniuj sekwencję wspólnych ustawień w wartości i dodaj je do każdego projektu. Mniej pojęć do nauczenia się w ten sposób.

na przykład

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)

Nawigacja projektów

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

Wtyczki

Pamiętaj, że definicja kompilacji to właściwy projekt Scala, który znajduje się w project/. W tym miejscu definiujemy wtyczkę, tworząc .scalapliki

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

Oto minimalne auto plugin podproject/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
    }   
  )
}

Zastąpienie

override def requires = plugins.JvmPlugin

powinny skutecznie włączyć wtyczkę dla wszystkich podprojektów bez konieczności wzywania wyraźnie enablePluginw build.sbt.

IntelliJ i sbt

Włącz następujące ustawienie (które powinno być domyślnie włączone )

use sbt shell

pod

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

Kluczowe odniesienia

Mario Galic
źródło