Instalasi
brew install sbt
atau instalasi serupa sbt yang secara teknis terdiri dari
Ketika Anda mengeksekusi sbt
dari terminal, itu benar-benar menjalankan skrip sbt launcher bash. Secara pribadi, saya tidak perlu khawatir tentang trinitas ini, dan hanya menggunakan sbt seolah-olah itu adalah satu hal.
Konfigurasi
Untuk mengkonfigurasi sbt untuk proyek tertentu, simpan .sbtopts
file di root proyek. Untuk mengkonfigurasi modifikasi sbt di seluruh sistem /usr/local/etc/sbtopts
. Pelaksana sbt -help
harus memberi tahu Anda lokasi yang tepat. Misalnya, untuk memberikan sbt lebih banyak memori saat satu kali dijalankan sbt -mem 4096
, atau simpan -mem 4096
dalam .sbtopts
atau sbtopts
agar peningkatan memori berlaku secara permanen.
Struktur proyek
sbt new scala/scala-seed.g8
membuat struktur proyek Hello World sbt minimal
.
├── 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
Perintah yang sering
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
Segudang cangkang
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
Definisi build adalah proyek Scala yang tepat
Ini adalah salah satu konsep kunci idiomatik sbt. Saya akan mencoba menjelaskan dengan sebuah pertanyaan. Katakanlah Anda ingin mendefinisikan tugas sbt yang akan menjalankan permintaan HTTP dengan scalaj-http. Secara intuitif kita mungkin mencoba yang berikut di dalambuild.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
...
}
Namun ini akan kesalahan mengatakan hilang import scalaj.http._
. Bagaimana ini mungkin ketika kita, tepat di atas, ditambahkan scalaj-http
ke libraryDependencies
? Lebih jauh, mengapa ini berfungsi ketika, sebagai gantinya, kita menambahkan ketergantungan ke project/build.sbt
?
// project/build.sbt
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
Jawabannya adalah itu fooTask
sebenarnya adalah bagian dari proyek Scala yang terpisah dari proyek utama Anda. Proyek Scala yang berbeda ini dapat ditemukan di bawah project/
direktori yang memiliki target/
direktori sendiri tempat kelas yang dikompilasinya berada. Sebenarnya, di bawah project/target/config-classes
harus ada kelas yang mendekompilasi menjadi seperti
object $9c2192aea3f1db3c251d extends scala.AnyRef {
lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
lazy val root : sbt.Project = { /* compiled code */ }
}
Kami melihat itu fooTask
hanyalah anggota dari objek Scala biasa bernama $9c2192aea3f1db3c251d
. Jelas scalaj-http
harus menjadi ketergantungan proyek yang mendefinisikan $9c2192aea3f1db3c251d
dan bukan ketergantungan proyek yang tepat. Oleh karena itu perlu dideklarasikan project/build.sbt
sebagai pengganti build.sbt
, karena di project
situlah definisi build proyek Scala berada.
Untuk menunjukkan bahwa definisi build hanyalah proyek Scala lainnya, jalankan sbt consoleProject
. Ini akan memuat Scala REPL dengan proyek definisi build di classpath. Anda akan melihat impor di sepanjang baris
import $9c2192aea3f1db3c251d
Jadi sekarang kita dapat berinteraksi langsung dengan proyek definisi pembangunan dengan memanggilnya dengan Scala proper, bukan build.sbt
DSL. Misalnya, eksekusi berikut inifooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbt
under root project adalah DSL khusus yang membantu menentukan definisi build proyek Scala di bawah project/
.
Dan membangun definisi proyek Scala, dapat memiliki proyek Scala definisi membangun sendiri di bawah project/project/
dan seterusnya. Kami mengatakan sbt bersifat rekursif .
sbt sejajar secara default
sbt membangun DAG dari tugas. Ini memungkinkannya untuk menganalisis ketergantungan antara tugas dan menjalankannya secara paralel dan bahkan melakukan deduplikasi. build.sbt
DSL dirancang dengan pemikiran ini, yang mungkin mengarah pada semantik awalnya yang mengejutkan. Menurut Anda, bagaimana urutan eksekusi dalam cuplikan berikut?
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
}
Secara intuitif orang mungkin berpikir aliran di sini adalah mencetak terlebih dahulu hello
kemudian mengeksekusi a
, dan kemudian b
tugas. Namun hal ini sebenarnya berarti mengeksekusi a
dan b
di paralel , dan sebelum println("hello")
jadi
a
b
hello
atau karena urutan a
dan b
tidak dijamin
b
a
hello
Mungkin secara paradoks, di sbt lebih mudah melakukan paralel daripada serial. Jika Anda memerlukan pengurutan serial, Anda harus menggunakan hal-hal khusus seperti Def.sequential
atau Def.taskDyn
untuk meniru pemahaman .
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
mirip dengan
for {
h <- Future(println("hello"))
a <- Future(println("a"))
b <- Future(println("b"))
} yield ()
di mana kami melihat tidak ada ketergantungan antar komponen, sementara
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
mirip dengan
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 }
di mana kita melihat sum
tergantung dan harus menunggu a
dan b
.
Dengan kata lain
- untuk semantik aplikatif , gunakan
.value
- untuk penggunaan semantik monad
sequential
atautaskDyn
Pertimbangkan cuplikan lain yang secara semantik membingungkan sebagai akibat dari sifat membangun ketergantungan value
, where, dan bukannya
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
^
kita harus menulis
val x = settingKey[String]("")
x := version.value
Perhatikan bahwa sintaks .value
adalah tentang hubungan di DAG dan tidak berarti
"beri aku nilainya sekarang"
sebaliknya itu berarti sesuatu seperti
"Penelepon saya bergantung pada saya dulu, dan setelah saya tahu bagaimana seluruh DAG cocok, saya akan dapat memberikan nilai yang diminta kepada penelepon saya"
Jadi sekarang mungkin sedikit lebih jelas mengapa x
belum bisa diberi nilai; belum ada nilai yang tersedia dalam tahap membangun hubungan.
Kami dapat dengan jelas melihat perbedaan dalam semantik antara Scala proper dan bahasa DSL di build.sbt
. Berikut adalah beberapa aturan praktis yang cocok untuk saya
- DAG terbuat dari ekspresi tipe
Setting[T]
- Dalam kebanyakan kasus kami hanya menggunakan
.value
sintaks dan sbt akan menjaga hubungan antaraSetting[T]
- Kadang-kadang kami harus mengubah bagian DAG secara manual dan untuk itu kami menggunakan
Def.sequential
atauDef.taskDyn
- Setelah keanehan sintatis pengurutan / hubungan ini diatasi, kita dapat mengandalkan semantik Scala yang biasa untuk membangun logika bisnis tugas lainnya.
Perintah vs Tugas
Perintah adalah jalan keluar malas dari DAG. Dengan menggunakan perintah, mudah untuk mengubah status build dan membuat serial tugas sesuai keinginan. Kerugiannya adalah kita kehilangan kesejajaran dan deduplikasi tugas yang disediakan oleh DAG, yang mana tugas harus menjadi pilihan yang lebih disukai. Anda dapat menganggap perintah sebagai semacam rekaman permanen dari sesi yang mungkin dilakukan di dalam sbt shell
. Misalnya, diberikan
vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value
pertimbangkan hasil dari sesi berikutnya
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
Secara khusus, bukan cara kami mengubah status build dengan set x := 41
. Perintah memungkinkan kita membuat rekaman permanen dari sesi di atas, misalnya
commands += Command.command("cmd") { state =>
"x" :: "show f" :: "set x := 41" :: "show f" :: state
}
Kita juga bisa membuat perintah type-safe menggunakan Project.extract
danrunTask
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
}
Cakupan
Cakupan mulai berperan saat kami mencoba menjawab jenis pertanyaan berikut
- Bagaimana cara mendefinisikan tugas sekali dan membuatnya tersedia untuk semua subproyek dalam multiproyek build?
- Bagaimana cara menghindari ketergantungan pengujian pada jalur kelas utama?
sbt memiliki ruang lingkup multi-sumbu yang dapat dinavigasi menggunakan sintaks garis miring , misalnya,
show root / Compile / compile / scalacOptions
| | | |
project configuration task key
Secara pribadi, saya jarang merasa khawatir tentang ruang lingkup. Terkadang saya hanya ingin mengumpulkan sumber pengujian
Test/compile
atau mungkin menjalankan tugas tertentu dari subproyek tertentu tanpa harus menavigasi ke proyek itu terlebih dahulu dengan project subprojB
subprojB/Test/compile
Saya pikir aturan praktis berikut membantu menghindari komplikasi pelingkupan
- tidak memiliki banyak
build.sbt
file tetapi hanya satu file master di bawah proyek root yang mengontrol semua subproyek lainnya
- berbagi tugas melalui plugin otomatis
- faktor keluar pengaturan umum menjadi Scala biasa
val
dan secara eksplisit menambahkannya ke setiap sub-proyek
Pembangunan multi-proyek
Alih-alih beberapa file build.sbt untuk setiap subproyek
.
├── 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
Miliki satu master build.sbt
untuk mengatur semuanya
.
├── 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
Ada praktik umum dalam memfaktorkan setelan umum dalam build multi-project
menentukan urutan pengaturan umum di val dan menambahkannya ke setiap proyek. Lebih sedikit konsep untuk dipelajari dengan cara itu.
sebagai contoh
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)
Proyek navigasi
projects // list all projects
project multi1 // change to particular project
Plugin
Ingat definisi build adalah proyek Scala yang tepat yang berada di bawah project/
. Di sinilah kami mendefinisikan plugin dengan membuat .scala
file
. // 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
Berikut adalah plugin otomatis minimal di bawahproject/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
}
)
}
Timpa
override def requires = plugins.JvmPlugin
harus efektif mengaktifkan plugin untuk semua sub-proyek tanpa harus memanggil secara eksplisit enablePlugin
dalam build.sbt
.
IntelliJ dan sbt
Harap aktifkan pengaturan berikut (yang seharusnya diaktifkan secara default )
use sbt shell
dibawah
Preferences | Build, Execution, Deployment | sbt | sbt projects
Referensi kunci