SBT 是 scala 上一個通用的 build tool,除了沿用 ivy 進行 library dependency 管理之外,還增加了更多 console 互動的指令,而且可以直接啟動一個新的 interpreter,進行 code testing。
Installation
在 mac 安裝 sbt,可以用 port 或是 brew
port install sbt
brew install sbt
在 windows,就下載 msi 安裝包,直接安裝
如果要自己手動安裝,必須先下載 sbt-launch.jar,參考 Installing sbt manually 的說明,建立 script
> vi ~/bin/sbt
#!/bin/bash
SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M -Dfile.encoding=UTF8"
java $SBT_OPTS -jar `dirname $0`/sbt-launch.jar "$@"
first simple project test
建立一個 sbttest 目錄,在該目錄中,執行 sbt,會進入 sbt console
> help
列印 sbt 基本的指令
> tasks
列出可以使用的 build task
> settings
列出我們可以修改的 settings
> inspect
查詢 setting/task 的資訊
列印 scala source folder
> scalaSource
[info] /project/idea/sbttest/src/main/scala
sbt project 的基本結構如下
<build directory>/
project/ sbt plugins and build help code
src/
main/
scala/ scala source code
java/ java source code
resources/ 要放在 classpath 但又不需要編譯的資源檔案
test/
scala/
java/
resources/
target/
build.sbt build file
buildscript.sh
#!/bin/bash
mkdir -p project
mkdir -p src/{main,test}/{scala,java,resources}
mkdir -p target
建立兩個檔案
vi build.sbt
name := "sbttest"
version := "1.0"
vi project/build.properties
sbt.version=0.13.7
建立一個 HelloWorld scala source
vi src/main/scala/HelloWorld.scala
object HelloWorld extends App {
println("Hello, sbt world!")
}
回到 sbt console,執行 compile task,然後就能直接 run
> compile
[info] Updating {file:/project/idea/sbttest/}sbttest...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Scala source to /project/idea/sbttest/target/scala-2.10/classes...
[success] Total time: 2 s, completed 2016/5/12 上午 10:54:37
> run
[info] Running HelloWorld
Hello, sbt world!
[success] Total time: 0 s, completed 2016/5/12 上午 10:55:48
增修以下的檔案
vi src/main/scala/models.scala
case class Product(id: Long,
attributes: Seq[String])
case class BuyerPreferences(attributes: Seq[String])
vi src/main/scala/logic.scala
object Logic {
// 判斷是否有吻合 buyer 的喜好特徵
def matchLikelihood(product: Product, buyer: BuyerPreferences): Double = {
val matches = buyer.attributes map { attribute =>
product.attributes contains attribute
}
val nums = matches map { b => if(b) 1.0 else 0.0 }
if (nums.length > 0) nums.sum / nums.length else 0.0
}
}
在 sbt 中,可以直接以 console 指令進入 scala interpreter
> console
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.10.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_92).
Type in expressions to have them evaluated.
Type :help for more information.
scala>
可以直接使用剛剛編譯的 scala 物件
// 產生 Product p1
scala> val p1 = Product(id=100, attributes = Seq("female", "kid-friendly"))
p1: Product = Product(100,List(female, kid-friendly))
// 產生 BuyerPreferences
scala> val prefs = BuyerPreferences(List("male", "kid-friendly"))
prefs: BuyerPreferences = BuyerPreferences(List(male, kid-friendly))
// 產生 Product p2
scala> val p2 = Product(id=110, attributes = Seq("male", "kid-friendly"))
p2: Product = Product(110,List(male, kid-friendly))
// 檢查 p1 的 attributes 是否有吻合 prefs 的 atrributes
scala> prefs.attributes.map(attribute => p1.attributes.contains(attribute))
res1: Seq[Boolean] = List(false, true)
// 將吻合的特性標記為 分數 1.0
scala> res1 map (matched => if(matched) 1.0 else 0)
res2: Seq[Double] = List(0.0, 1.0)
// 計算總得分
scala> res2.sum / res2.length
res3: Double = 0.5
利用 specs 進行 unit test
首先在 build.sbt 增加一行
libraryDependencies += "org.specs2" % "specs2_2.10" % "1.14" % "test"
增加一個測試程式
import org.specs2.mutable.Specification
object LogicSpec extends Specification {
"The 'matchLikelihood' method" should {
"be 100% when all attributes match" in {
val tabby = Product(1, List("male", "tabby"))
val prefs = BuyerPreferences(List("male", "tabby"))
Logic.matchLikelihood(tabby, prefs) must beGreaterThan(0.999)
}
"be 0% when no attributes match" in {
val tabby = Product(1, List("male", "tabby"))
val prefs = BuyerPreferences(List("female", "calico"))
val result = Logic.matchLikelihood(tabby, prefs)
result must beLessThan(0.001)
}
"correctly handle an empty BuyerPreferences" in {
val tabby = Product(1, List("male", "tabby"))
val prefs = BuyerPreferences(List())
val result = Logic.matchLikelihood(tabby, prefs)
result.isNaN mustEqual false
}
}
}
回到 sbt console(記得要跳出 scala interpreter),以 reload 重新載入 project 設定,以 test 進行 unit test
> reload
[info] Loading project definition from /project/idea/book/sbt-in-action-examples-master/chapter2/project
[info] Set current project to preowned-kittens (in build file:/project/idea/book/sbt-in-action-examples-master/chapter2/)
> test
[info] Compiling 1 Scala source to /project/idea/book/sbt-in-action-examples-master/chapter2/target/scala-2.10/test-classes...
[info] LogicSpec
[info]
[info] The 'matchLikelihood' method should
[info] + be 100% when all attributes match
[info] + be 0% when no attributes match
[info] + correctly handle an empty BuyerPreferences
[info]
[info]
[info] Total for specification LogicSpec
[info] Finished in 68 ms
[info] 3 examples, 0 failure, 0 error
[info]
[info] Passed: Total 3, Failed 0, Errors 0, Passed 3
[success] Total time: 4 s, completed 2016/5/12 上午 11:23:04
sbt 的測試還有另一個模式 ~test,可以持續等待 source code 的更新,並在更新後,自動執行 unit test,當 source code 有任何異動時,他會自動編譯並執行測試,在這個模式下的測試非常地有效率。
> ~test
[info] LogicSpec
[info]
[info] The 'matchLikelihood' method should
[info] + be 100% when all attributes match
[info] + be 0% when no attributes match
[info] + correctly handle an empty BuyerPreferences
[info]
[info]
[info] Total for specification LogicSpec
[info] Finished in 15 ms
[info] 3 examples, 0 failure, 0 error
[info]
[info] Passed: Total 3, Failed 0, Errors 0, Passed 3
[success] Total time: 1 s, completed 2016/5/12 上午 11:36:08
1. Waiting for source changes... (press enter to interrupt)
如果只要進行某一個單元測試,就要用 testOnly
> testOnly LogicSpec
如何定義 build.sbt
settings 裡面有三個 operators 用來建立 settings
- := 將新的 value 複寫原本的 key
- += 將新的 value 附加到原本 key 所儲存的 sequence 裡面
- ++= 將新的 sequence of values 附加到原本 key 所儲存的 sequence 裡面
定義 ModuleID 的格式為
"groupId" % "artifactId" % "version"
例如 libraryDependencies 裡面就存放著 sequence of library dependencies,如果要定義兩個以上的 libraries,就要用以下的寫法
libraryDependencies ++= Seq(
"junit" % "junit" % "4.11" % "test",
"org.specs2" % "specs2_2.10" % "1.10" % "test"
)
自訂 key 的方式如下:gitHeadCommitSha 是 key 的名稱,型別為 String,而真實的值是由 scala.sys.process 裡面的 Process("git rev-parse HEAD") 運算得來的
val gitHeadCommitSha = taskKey[String]("Determines the current git commit SHA")
gitHeadCommitSha := Process("git rev-parse HEAD").lines.head
如果在一個 git project 裡面直接執行此指令,可以取得 hash value,所以上面的 gitHeadCommitSha 其實就是取得這個 hash value
> git rev-parse HEAD
8a0c542b032c78262f9e3a9a60cef318290c7d99
parallel execution
當 taskA depends on taskB, taskC,sbt 會嘗試同時執行 taskB與taskC,以下是驗證的方式:taskB, taskC 都會暫停 5 秒鐘,但 taskA 執行的時候,也是暫停 5s,這表示 taskB, taskC 確實是平行執行的
val taskA = taskKey[String]("taskA")
val taskB = taskKey[String]("taskB")
val taskC = taskKey[String]("taskC")
taskA := { val b = taskB.value; val c = taskC.value; "taskA" }
taskB := { Thread.sleep(5000); "taskB" }
taskC := { Thread.sleep(5000); "taskC" }
執行結果
> taskA
[success] Total time: 5 s, completed 2016/5/12 下午 03:19:12
> taskB
[success] Total time: 5 s, completed 2016/5/12 下午 03:19:30
subproject
sbt 支援 subproject,可以在子資料夾中,定義另一個 subproject,並在編譯時決定 project dependency。
在 build.sbt 中定義 common project,並以子資料夾 common 為 subproject 目錄
lazy val common = (
Project("common", file("common")).
settings()
)
在 sbt console可以用 projects 指令查閱
> projects
[info] In file:/project/idea/book/sbt-in-action-examples-master/chapter3/
[info] * chapter3
[info] temp
[info] website
如果先定義一個共用的 project function: PreownedKittenProject,就可以在後面其他的 project 定義中,直接呼叫該 project function。
// Common settings/definitions for the build
def PreownedKittenProject(name: String): Project = (
Project(name, file(name))
settings(
libraryDependencies += "org.specs2" % "specs2_2.10" % "1.14" % "test"
)
)
lazy val common = (
PreownedKittenProject("common")
settings()
)
lazy val analytics = (
PreownedKittenProject("analytics")
dependsOn(common)
settings()
)
lazy val website = (
PreownedKittenProject("website")
dependsOn(common)
settings()
)
compile, run, test, package, publish
利用 inspect tree compile:compile 指令,可查詢編譯時,所需要的資源 tree,而 inspect tree sources 可查閱原始程式的 tree。
> inspect tree compile:compile
[info] chapter4/compile:compile = Task[sbt.inc.Analysis]
[info] +-chapter4/compile:compile::compileInputs = Task[sbt.Compiler$Inputs]
[info] | +-chapter4/compile:classDirectory = target/scala-2.10/classes
[info] | +-*/*:compileOrder = Mixed
[info] | +-chapter4/*:compilers = Task[sbt.Compiler$Compilers]
[info] | +-chapter4/compile:dependencyClasspath = Task[scala.collection.Seq[sbt.Attributed[jav..
[info] | +-chapter4/compile:incCompileSetup = Task[sbt.Compiler$IncSetup]
[info] | +-*/*:javacOptions = Task[scala.collection.Seq[java.lang.String]]
[info] | +-*/*:maxErrors = 100
[info] | +-chapter4/compile:scalacOptions = Task[scala.collection.Seq[java.lang.String]]
[info] | +-*/*:sourcePositionMappers = Task[scala.collection.Seq[scala.Function1[xsbti.Positio..
[info] | +-chapter4/compile:sources = Task[scala.collection.Seq[java.io.File]]
[info] | +-chapter4/compile:compile::streams = Task[sbt.std.TaskStreams[sbt.Init$ScopedKey[_ <..
[info] | +-*/*:streamsManager = Task[sbt.std.Streams[sbt.Init$ScopedKey[_ <: Any]]]
[info] |
[info] +-chapter4/compile:compile::compilerReporter = Task[scala.Option[xsbti.Reporter]]
[info] +-chapter4/compile:compile::streams = Task[sbt.std.TaskStreams[sbt.Init$ScopedKey[_ <: ..
[info] +-*/*:streamsManager = Task[sbt.std.Streams[sbt.Init$ScopedKey[_ <: Any]]]
[info]
> inspect tree sources
[info] chapter4/compile:sources = Task[scala.collection.Seq[java.io.File]]
[info] +-chapter4/compile:managedSources = Task[scala.collection.Seq[java.io.File]]
[info] | +-chapter4/compile:sourceGenerators = List()
[info] |
[info] +-chapter4/compile:unmanagedSources = Task[scala.collection.Seq[java.io.File]]
[info] +-chapter4/*:baseDirectory = /Users/charley/project/idea/book/sbt-in-action-examples-..
[info] +-*/*:sourcesInBase = true
[info] +-chapter4/compile:unmanagedSourceDirectories = List(/Users/charley/project/idea/book..
[info] | +-chapter4/compile:javaSource = src/main/java
[info] | | +-chapter4/compile:sourceDirectory = src/main
[info] | | +-chapter4/*:sourceDirectory = src
[info] | | | +-chapter4/*:baseDirectory = /Users/charley/project/idea/book/sbt-in-action-e..
[info] | | | +-chapter4/*:thisProject = Project(id chapter4, base: /Users/charley/projec..
[info] | | |
[info] | | +-chapter4/compile:configuration = compile
[info] | |
[info] | +-chapter4/compile:scalaSource = src/main/scala
[info] | +-chapter4/compile:sourceDirectory = src/main
[info] | +-chapter4/*:sourceDirectory = src
[info] | | +-chapter4/*:baseDirectory = /Users/charley/project/idea/book/sbt-in-action-e..
[info] | | +-chapter4/*:thisProject = Project(id chapter4, base: /Users/charley/projec..
[info] | |
[info] | +-chapter4/compile:configuration = compile
[info] |
[info] +-*/*:excludeFilter = sbt.HiddenFileFilter$@731a5a39
[info] +-*/*:unmanagedSources::includeFilter = sbt.SimpleFilter@1acd952e
[info]
> inspect tree test:sources
>
> inspect tree compile:dependencyClasspath
- unmanagedSources 既有的 project convernsions 取得的 a list of source files
- managedSources 手動加入的 sources list
# 查閱 java source folders
> show javaSource
# 查閱 scala source folders
> show scalaSource
# 查閱資源目錄
> show resourceDirectory
sbt 預設會使用以下的 libray repositories
- Bintray's JCenter
- Maven Central
- Typesafe releases
- sbt community releases
沒有留言:
張貼留言