การติดตั้ง
brew install sbt
หรือการติดตั้งที่คล้ายกัน sbt ซึ่งในทางเทคนิคประกอบด้วย
เมื่อคุณเรียกใช้งานsbt
จากเทอร์มินัลมันจะเรียกใช้สคริปต์ bash ตัวเรียกใช้ sbt โดยส่วนตัวฉันไม่เคยต้องกังวลเกี่ยวกับไตรลักษณ์นี้และใช้ sbt ราวกับว่ามันเป็นสิ่งเดียว
การกำหนดค่า
ในการกำหนดค่า sbt สำหรับ.sbtopts
ไฟล์บันทึกโปรเจ็กต์เฉพาะที่รูทของโปรเจ็กต์ การกำหนดค่าทั้งระบบ SBT /usr/local/etc/sbtopts
แก้ไข การดำเนินการsbt -help
ควรบอกตำแหน่งที่แน่นอน ตัวอย่างเช่นการให้ SBT หน่วยความจำมากขึ้นเป็น one-off ดำเนินการsbt -mem 4096
หรือบันทึก-mem 4096
ใน.sbtopts
หรือsbtopts
การเพิ่มหน่วยความจำจะมีผลอย่างถาวร
โครงสร้างโครงการ
sbt new scala/scala-seed.g8
สร้างโครงสร้างโครงการ 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
คำสั่งที่ใช้บ่อย
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
เปลือกหอยมากมาย
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
Build definition เป็นโครงการ Scala ที่เหมาะสม
นี่เป็นหนึ่งในแนวคิดหลักเกี่ยวกับ SBT ที่เป็นสำนวน ฉันจะพยายามอธิบายด้วยคำถาม สมมติว่าคุณต้องการกำหนดงาน sbt ที่จะดำเนินการตามคำขอ HTTP ด้วย scalaj-http โดยสัญชาตญาณเราอาจลองทำสิ่งต่อไปนี้ภายในbuild.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
...
}
import scalaj.http._
อย่างไรก็ตามเรื่องนี้ผิดพลาดจะพูดว่าหายไป จะเป็นไปได้อย่างไรเมื่อเราถูกเพิ่มเข้าไปscalaj-http
ในlibraryDependencies
? นอกจากนี้ทำไมมันทำงานเมื่อแทนเราเพิ่มขึ้นต่อไปproject/build.sbt
?
// project/build.sbt
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
คำตอบคือfooTask
จริงๆแล้วเป็นส่วนหนึ่งของโครงการ Scala ที่แยกจากโครงการหลักของคุณ โครงการ Scala ที่แตกต่างกันนี้สามารถพบได้ภายใต้project/
ไดเร็กทอรีซึ่งมีtarget/
ไดเร็กทอรีของตัวเองซึ่งมีคลาสที่คอมไพล์อยู่ ในความเป็นจริงproject/target/config-classes
ควรมีคลาสที่ถอดรหัสเป็นบางอย่างเช่น
object $9c2192aea3f1db3c251d extends scala.AnyRef {
lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
lazy val root : sbt.Project = { /* compiled code */ }
}
เราจะเห็นว่าfooTask
เป็นเพียงสมาชิกคนหนึ่งของวัตถุ Scala $9c2192aea3f1db3c251d
ปกติชื่อ เห็นได้ชัดว่าscalaj-http
ควรเป็นการพึ่งพาของการกำหนดโครงการ$9c2192aea3f1db3c251d
ไม่ใช่การพึ่งพาโครงการที่เหมาะสม ดังนั้นจึงจำเป็นต้องมีการประกาศproject/build.sbt
แทนbuild.sbt
เนื่องจากproject
เป็นที่ที่โครงการ Scala นิยามการสร้างอยู่
เพื่อผลักดันให้จุดที่สร้างความหมายเป็นเพียงอีกหนึ่งโครงการ Scala sbt consoleProject
รัน สิ่งนี้จะโหลด Scala REPL ด้วยโปรเจ็กต์คำจำกัดความการสร้างบนคลาสพา ธ คุณควรเห็นการนำเข้าตามบรรทัดของ
import $9c2192aea3f1db3c251d
ตอนนี้เราสามารถโต้ตอบโดยตรงกับโครงการนิยามการสร้างโดยเรียกมันว่า Scala ที่เหมาะสมแทนbuild.sbt
DSL ตัวอย่างเช่นการดำเนินการต่อไปนี้fooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbt
ภายใต้โครงการหลักเป็น DSL spcial project/
ที่ช่วยกำหนดสร้างข้อกำหนดของโครงการภายใต้
และสร้างนิยามโครงการ Scala สามารถมีโครงการสร้างนิยาม Scala ภายใต้project/project/
และอื่น ๆ เราพูดSBT เป็น recursive
sbt ขนานกันโดยค่าเริ่มต้น
sbt สร้างDAGออกจากงาน สิ่งนี้ช่วยให้สามารถวิเคราะห์การอ้างอิงระหว่างงานและดำเนินการควบคู่กันไปและแม้แต่ทำการคัดลอกข้อมูลซ้ำซ้อน build.sbt
DSL ได้รับการออกแบบโดยคำนึงถึงสิ่งนี้ซึ่งอาจนำไปสู่ความหมายที่น่าแปลกใจในตอนแรก คุณคิดว่าลำดับการดำเนินการเป็นอย่างไรในตัวอย่างต่อไปนี้
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
}
โดยสัญชาตญาณอาจคิดว่าขั้นตอนที่นี่คือการพิมพ์ครั้งแรกhello
จากนั้นดำเนินการa
แล้วจึงb
ทำงาน อย่างไรก็ตามเรื่องนี้จริงหมายถึงการดำเนินการa
และb
ในแบบขนานและก่อน println("hello")
ดังนั้น
a
b
hello
หรือเนื่องจากคำสั่งซื้อa
และb
ไม่รับประกัน
b
a
hello
บางทีขัดแย้งกันใน sbt มันง่ายกว่าที่จะทำแบบขนานกว่าอนุกรม หากคุณจำเป็นต้องสั่งซื้อแบบอนุกรมที่คุณจะต้องใช้สิ่งที่พิเศษเช่นDef.sequential
หรือDef.taskDyn
จะเลียนแบบสำหรับความเข้าใจ
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
เหมือนกับ
for {
h <- Future(println("hello"))
a <- Future(println("a"))
b <- Future(println("b"))
} yield ()
ที่เราเห็นว่าไม่มีการพึ่งพาระหว่างส่วนประกอบในขณะที่
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
เหมือนกับ
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 }
ที่เราเห็นsum
ขึ้นอยู่กับและมีการรอและa
b
กล่าวอีกนัยหนึ่ง
- สำหรับความหมายเชิงประยุกต์ใช้
.value
- สำหรับการใช้ความหมายแบบ monadic
sequential
หรือtaskDyn
พิจารณาตัวอย่างข้อมูลอื่นที่สับสนทางความหมายอันเป็นผลมาจากลักษณะการสร้างการพึ่งพาvalue
ซึ่งแทนที่จะเป็น
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
^
เราต้องเขียน
val x = settingKey[String]("")
x := version.value
โปรดทราบว่าไวยากรณ์.value
เป็นเรื่องเกี่ยวกับความสัมพันธ์ใน DAG และไม่ได้หมายความว่า
"ให้ค่าฉันตอนนี้"
แทนหมายถึง
"ผู้โทรของฉันขึ้นอยู่กับฉันก่อนและเมื่อฉันรู้ว่า DAG ทั้งหมดเข้ากันได้อย่างไรฉันจะสามารถให้ค่าที่ร้องขอแก่ผู้โทรได้"
ตอนนี้มันอาจจะชัดเจนขึ้นเล็กน้อยว่าทำไมx
ยังไม่สามารถกำหนดค่าได้ ยังไม่มีค่าในขั้นตอนการสร้างความสัมพันธ์
เราสามารถเห็นความแตกต่างอย่างชัดเจนในความหมายระหว่างภาษา Scala ที่เหมาะสมกับภาษา DSL ในbuild.sbt
. นี่คือกฎเล็ก ๆ น้อย ๆ ที่เหมาะกับฉัน
- DAG สร้างขึ้นจากนิพจน์ประเภท
Setting[T]
- ในกรณีส่วนใหญ่เราใช้
.value
ไวยากรณ์และ sbt จะดูแลการสร้างความสัมพันธ์ระหว่างSetting[T]
- บางครั้งเราต้องปรับแต่งส่วนหนึ่งของ DAG ด้วยตนเองและสำหรับสิ่งที่เราใช้
Def.sequential
หรือDef.taskDyn
- เมื่อได้รับการดูแลความผิดปกติทางไวยากรณ์ในการสั่งซื้อ / ความสัมพันธ์แล้วเราสามารถใช้ความหมายปกติของ Scala ในการสร้างตรรกะทางธุรกิจที่เหลือของงานได้
คำสั่งเทียบกับงาน
คำสั่งเป็นวิธีที่ขี้เกียจออกจาก DAG การใช้คำสั่งทำให้ง่ายต่อการเปลี่ยนสถานะการสร้างและงานต่อเนื่องตามที่คุณต้องการ ค่าใช้จ่ายคือเราสูญเสียการขนานและการคัดลอกงานที่จัดทำโดย DAG ซึ่งวิธีใดควรเป็นทางเลือกที่ต้องการ sbt shell
คุณสามารถคิดว่าคำสั่งเป็นชนิดของการบันทึกถาวรของหนึ่งเซสชั่นอาจทำภายใน ตัวอย่างเช่นให้
vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value
พิจารณาผลลัพธ์ของเซสชันต่อไปนี้
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
set x := 41
โดยเฉพาะอย่างยิ่งไม่ว่าเรากลายพันธุ์สร้างรัฐที่มี คำสั่งช่วยให้เราสามารถบันทึกเซสชันข้างต้นได้อย่างถาวรเช่น
commands += Command.command("cmd") { state =>
"x" :: "show f" :: "set x := 41" :: "show f" :: state
}
นอกจากนี้เรายังสามารถทำให้คำสั่งประเภทปลอดภัยโดยใช้Project.extract
และrunTask
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
}
ขอบเขต
ขอบเขตเข้ามามีบทบาทเมื่อเราพยายามตอบคำถามประเภทต่อไปนี้
- จะกำหนดงานเพียงครั้งเดียวและทำให้พร้อมใช้งานสำหรับโครงการย่อยทั้งหมดในการสร้างหลายโครงการได้อย่างไร
- จะหลีกเลี่ยงการพึ่งพาการทดสอบบนคลาสพา ธ หลักได้อย่างไร?
sbt มีพื้นที่กำหนดขอบเขตหลายแกนซึ่งสามารถนำทางได้โดยใช้ไวยากรณ์สแลชตัวอย่างเช่น
show root / Compile / compile / scalacOptions
| | | |
project configuration task key
โดยส่วนตัวฉันไม่ค่อยพบว่าตัวเองต้องกังวลเรื่องขอบเขต บางครั้งฉันต้องการรวบรวมเพียงแค่แหล่งทดสอบ
Test/compile
หรืออาจดำเนินการงานเฉพาะจากโปรเจ็กต์ย่อยเฉพาะโดยไม่ต้องไปที่โปรเจ็กต์นั้นก่อนด้วย project subprojB
subprojB/Test/compile
ฉันคิดว่ากฎง่ายๆต่อไปนี้ช่วยหลีกเลี่ยงการกำหนดขอบเขตภาวะแทรกซ้อน
- ไม่มี
build.sbt
ไฟล์หลายไฟล์ แต่มีเพียงมาสเตอร์เดียวภายใต้โปรเจ็กต์รูทที่ควบคุมโปรเจ็กต์ย่อยอื่น ๆ ทั้งหมด
- แบ่งปันงานผ่านปลั๊กอินอัตโนมัติ
- แยกการตั้งค่าทั่วไปออกเป็น Scala ธรรมดา
val
และเพิ่มลงในแต่ละโครงการย่อยอย่างชัดเจน
การสร้างหลายโครงการ
แทนที่จะเป็นไฟล์ build.sbt หลายไฟล์สำหรับแต่ละโปรเจ็กต์ย่อย
.
├── 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
มีนายคนเดียวที่build.sbt
จะปกครองพวกเขาทั้งหมด
.
├── 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
มีแนวทางปฏิบัติทั่วไปในการแยกการตั้งค่าทั่วไปในการสร้างหลายโครงการ
กำหนดลำดับของการตั้งค่าทั่วไปใน val และเพิ่มเข้าไปในแต่ละโครงการ แนวคิดน้อยกว่าที่จะเรียนรู้ด้วยวิธีนั้น
ตัวอย่างเช่น
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)
การนำทางโครงการ
projects // list all projects
project multi1 // change to particular project
ปลั๊กอิน
project/
โปรดจำไว้ว่าการสร้างความหมายเป็นโครงการที่สกาล่าที่เหมาะสมที่อยู่ภายใต้ นี่คือที่ที่เรากำหนดปลั๊กอินโดยการสร้าง.scala
ไฟล์
. // 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
นี่คือปลั๊กอินอัตโนมัติขั้นต่ำที่อยู่ภายใต้project/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
}
)
}
การแทนที่
override def requires = plugins.JvmPlugin
ได้อย่างมีประสิทธิภาพควรเปิดใช้งานปลั๊กอินสำหรับโครงการย่อยทั้งหมดโดยไม่ต้องเรียกอย่างชัดเจนในenablePlugin
build.sbt
IntelliJ และ sbt
โปรดเปิดใช้งานการตั้งค่าต่อไปนี้ (ซึ่งควรเปิดใช้งานโดยค่าเริ่มต้น )
use sbt shell
ภายใต้
Preferences | Build, Execution, Deployment | sbt | sbt projects
การอ้างอิงที่สำคัญ