2016/9/19

SBT

SBT


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


  1. :=
    將新的 value 複寫原本的 key
  2. +=
    將新的 value 附加到原本 key 所儲存的 sequence 裡面
  3. ++=
    將新的 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

  1. unmanagedSources
    既有的 project convernsions 取得的 a list of source files
  2. 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

Reference


sbt in action