這堂課將概述SBT!具體議題包括:
SBT是一個(gè)現(xiàn)代化的構(gòu)建工具。雖然它由Scala編寫(xiě)并提供了很多Scala便利,但它是一個(gè)通用的構(gòu)建工具。
譯注?最新的SBT安裝方式請(qǐng)參考?scala-sbt的文檔
java -Xmx512M -jar sbt-launch.jar "$@"
[local ~/projects]$ sbt
Project does not exist, create new project? (y/N/s) y
Name: sample
Organization: com.twitter
Version [1.0]: 1.0-SNAPSHOT
Scala version [2.7.7]: 2.8.1
sbt version [0.7.4]:
Getting Scala 2.7.7 ...
:: retrieving :: org.scala-tools.sbt#boot-scala
confs: [default]
2 artifacts copied, 0 already retrieved (9911kB/221ms)
Getting org.scala-tools.sbt sbt_2.7.7 0.7.4 ...
:: retrieving :: org.scala-tools.sbt#boot-app
confs: [default]
15 artifacts copied, 0 already retrieved (4096kB/167ms)
[success] Successfully initialized directory structure.
Getting Scala 2.8.1 ...
:: retrieving :: org.scala-tools.sbt#boot-scala
confs: [default]
2 artifacts copied, 0 already retrieved (15118kB/386ms)
[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1
[info] using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7
>
可以看到它已經(jīng)以較好的形式創(chuàng)建了項(xiàng)目的快照版本。
我們將為簡(jiǎn)單的tweet消息創(chuàng)建一個(gè)簡(jiǎn)單的JSON解析器。將以下代碼加在這個(gè)文件中
src/main/scala/com/twitter/sample/SimpleParser.scala
package com.twitter.sample
case class SimpleParsed(id: Long, text: String)
class SimpleParser {
val tweetRegex = "\"id\":(.*),\"text\":\"(.*)\"".r
def parse(str: String) = {
tweetRegex.findFirstMatchIn(str) match {
case Some(m) => {
val id = str.substring(m.start(1), m.end(1)).toInt
val text = str.substring(m.start(2), m.end(2))
Some(SimpleParsed(id, text))
}
case _ => None
}
}
}
這段代碼丑陋并有bug,但應(yīng)該能夠編譯通過(guò)。
SBT既可以用作命令行腳本,也可以作為構(gòu)建控制臺(tái)。我們將主要利用它作為構(gòu)建控制臺(tái),不過(guò)大多數(shù)命令可以作為參數(shù)傳遞給SBT獨(dú)立運(yùn)行,如
sbt test
需要注意如果一個(gè)命令需要參數(shù),你需要使用引號(hào)包括住整個(gè)參數(shù)路徑,例如
sbt 'test-only com.twitter.sample.SampleSpec'
這種方式很奇怪。
不管怎樣,要開(kāi)始我們的代碼工作了,啟動(dòng)SBT吧
[local ~/projects/sbt-sample]$ sbt
[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1
[info] using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7
>
SBT允許你啟動(dòng)一個(gè)Scala REPL并加載所有項(xiàng)目依賴。它會(huì)在啟動(dòng)控制臺(tái)前編譯項(xiàng)目的源代碼,從而為我們提供一個(gè)快速測(cè)試解析器的工作臺(tái)。
> console
[info]
[info] == compile ==
[info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling main sources...
[info] Nothing to compile.
[info] Post-analysis: 3 classes.
[info] == compile ==
[info]
[info] == copy-test-resources ==
[info] == copy-test-resources ==
[info]
[info] == test-compile ==
[info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling test sources...
[info] Nothing to compile.
[info] Post-analysis: 0 classes.
[info] == test-compile ==
[info]
[info] == copy-resources ==
[info] == copy-resources ==
[info]
[info] == console ==
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.8.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_22).
Type in expressions to have them evaluated.
Type :help for more information.
scala>
我們代碼編譯通過(guò)了,并提供了典型的Scala提示符。我們將創(chuàng)建一個(gè)新的解析器,一個(gè)tweet以確保其“能工作”
scala> import com.twitter.sample._
import com.twitter.sample._
scala> val tweet = """{"id":1,"text":"foo"}"""
tweet: java.lang.String = {"id":1,"text":"foo"}
scala> val parser = new SimpleParser
parser: com.twitter.sample.SimpleParser = com.twitter.sample.SimpleParser@71060c3e
scala> parser.parse(tweet)
res0: Option[com.twitter.sample.SimpleParsed] = Some(SimpleParsed(1,"foo"}))
scala>
我們簡(jiǎn)單的解析器對(duì)這個(gè)非常小的輸入集工作正常,但我們需要添加更多的測(cè)試并讓它出錯(cuò)。第一步是在我們的項(xiàng)目中添加specs測(cè)試庫(kù)和一個(gè)真正的JSON解析器。要做到這一點(diǎn),我們必須超越默認(rèn)的SBT項(xiàng)目布局來(lái)創(chuàng)建一個(gè)項(xiàng)目。
SBT認(rèn)為project/build目錄中的Scala文件是項(xiàng)目定義。添加以下內(nèi)容到這個(gè)文件中project/build/SampleProject.scala
import sbt._
class SampleProject(info: ProjectInfo) extends DefaultProject(info) {
val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1"
val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test"
}
一個(gè)項(xiàng)目定義是一個(gè)SBT類。在上面例子中,我們擴(kuò)展了SBT的DefaultProject。
這里是通過(guò)val聲明依賴。SBT使用反射來(lái)掃描項(xiàng)目中的所有val依賴,并在構(gòu)建時(shí)建立依賴關(guān)系樹(shù)。這里使用的語(yǔ)法可能是新的,但本質(zhì)和Maven依賴是相同的
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.scala-tools.testing</groupId>
<artifactId>specs_2.8.0</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
現(xiàn)在可以下載我們的項(xiàng)目依賴了。在命令行中(而不是sbt console中)運(yùn)行sbt update
[local ~/projects/sbt-sample]$ sbt update
[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1
[info] using SampleProject with sbt 0.7.4 and Scala 2.7.7
[info]
[info] == update ==
[info] :: retrieving :: com.twitter#sample_2.8.1 [sync]
[info] confs: [compile, runtime, test, provided, system, optional, sources, javadoc]
[info] 1 artifacts copied, 0 already retrieved (2785kB/71ms)
[info] == update ==
[success] Successful.
[info]
[info] Total time: 1 s, completed Nov 24, 2010 8:47:26 AM
[info]
[info] Total session time: 2 s, completed Nov 24, 2010 8:47:26 AM
[success] Build completed successfully.
你會(huì)看到sbt檢索到specs庫(kù)。現(xiàn)在還增加了一個(gè)lib_managed目錄,并且在lib_managed/scala_2.8.1/test目錄中包含 specs_2.8.0-1.6.5.jar
現(xiàn)在有了測(cè)試庫(kù),可以把下面的測(cè)試代碼寫(xiě)入src/test/scala/com/twitter/sample/SimpleParserSpec.scala文件
package com.twitter.sample
import org.specs._
object SimpleParserSpec extends Specification {
"SimpleParser" should {
val parser = new SimpleParser()
"work with basic tweet" in {
val tweet = """{"id":1,"text":"foo"}"""
parser.parse(tweet) match {
case Some(parsed) => {
parsed.text must be_==("foo")
parsed.id must be_==(1)
}
case _ => fail("didn't parse tweet")
}
}
}
}
在SBT控制臺(tái)中運(yùn)行test
> test
[info]
[info] == compile ==
[info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling main sources...
[info] Nothing to compile.
[info] Post-analysis: 3 classes.
[info] == compile ==
[info]
[info] == test-compile ==
[info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling test sources...
[info] Nothing to compile.
[info] Post-analysis: 10 classes.
[info] == test-compile ==
[info]
[info] == copy-test-resources ==
[info] == copy-test-resources ==
[info]
[info] == copy-resources ==
[info] == copy-resources ==
[info]
[info] == test-start ==
[info] == test-start ==
[info]
[info] == com.twitter.sample.SimpleParserSpec ==
[info] SimpleParserSpec
[info] SimpleParser should
[info] + work with basic tweet
[info] == com.twitter.sample.SimpleParserSpec ==
[info]
[info] == test-complete ==
[info] == test-complete ==
[info]
[info] == test-finish ==
[info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0
[info]
[info] All tests PASSED.
[info] == test-finish ==
[info]
[info] == test-cleanup ==
[info] == test-cleanup ==
[info]
[info] == test ==
[info] == test ==
[success] Successful.
[info]
[info] Total time: 0 s, completed Nov 24, 2010 8:54:45 AM
>
我們的測(cè)試通過(guò)了!現(xiàn)在,我們可以增加更多。運(yùn)行觸發(fā)動(dòng)作是SBT提供的優(yōu)秀特性之一。在動(dòng)作開(kāi)始添加一個(gè)波浪線會(huì)啟動(dòng)一個(gè)循環(huán),在源文件發(fā)生變化時(shí)重新運(yùn)行動(dòng)作。讓我們運(yùn)行 ~test 并看看會(huì)發(fā)生什么吧。
[info] == test ==
[success] Successful.
[info]
[info] Total time: 0 s, completed Nov 24, 2010 8:55:50 AM
1\. Waiting for source changes... (press enter to interrupt)
現(xiàn)在,讓我們添加下面的測(cè)試案例
"reject a non-JSON tweet" in {
val tweet = """"id":1,"text":"foo""""
parser.parse(tweet) match {
case Some(parsed) => fail("didn't reject a non-JSON tweet")
case e => e must be_==(None)
}
}
"ignore nested content" in {
val tweet = """{"id":1,"text":"foo","nested":{"id":2}}"""
parser.parse(tweet) match {
case Some(parsed) => {
parsed.text must be_==("foo")
parsed.id must be_==(1)
}
case _ => fail("didn't parse tweet")
}
}
"fail on partial content" in {
val tweet = """{"id":1}"""
parser.parse(tweet) match {
case Some(parsed) => fail("didn't reject a partial tweet")
case e => e must be_==(None)
}
}
在我們保存文件后,SBT會(huì)檢測(cè)到變化,運(yùn)行測(cè)試,并通知我們的解析器有問(wèn)題
[info] == com.twitter.sample.SimpleParserSpec ==
[info] SimpleParserSpec
[info] SimpleParser should
[info] + work with basic tweet
[info] x reject a non-JSON tweet
[info] didn't reject a non-JSON tweet (Specification.scala:43)
[info] x ignore nested content
[info] 'foo","nested":{"id' is not equal to 'foo' (SimpleParserSpec.scala:31)
[info] + fail on partial content
因此,讓我們返工實(shí)現(xiàn)真正的JSON解析器
package com.twitter.sample
import org.codehaus.jackson._
import org.codehaus.jackson.JsonToken._
case class SimpleParsed(id: Long, text: String)
class SimpleParser {
val parserFactory = new JsonFactory()
def parse(str: String) = {
val parser = parserFactory.createJsonParser(str)
if (parser.nextToken() == START_OBJECT) {
var token = parser.nextToken()
var textOpt:Option[String] = None
var idOpt:Option[Long] = None
while(token != null) {
if (token == FIELD_NAME) {
parser.getCurrentName() match {
case "text" => {
parser.nextToken()
textOpt = Some(parser.getText())
}
case "id" => {
parser.nextToken()
idOpt = Some(parser.getLongValue())
}
case _ => // noop
}
}
token = parser.nextToken()
}
if (textOpt.isDefined && idOpt.isDefined) {
Some(SimpleParsed(idOpt.get, textOpt.get))
} else {
None
}
} else {
None
}
}
}
這是一個(gè)簡(jiǎn)單的Jackson解析器。當(dāng)我們保存,SBT會(huì)重新編譯代碼和運(yùn)行測(cè)試。代碼變得越來(lái)越好了!
info] SimpleParser should
[info] + work with basic tweet
[info] + reject a non-JSON tweet
[info] x ignore nested content
[info] '2' is not equal to '1' (SimpleParserSpec.scala:32)
[info] + fail on partial content
[info] == com.twitter.sample.SimpleParserSpec ==
哦。我們需要檢查嵌套對(duì)象。讓我們?cè)趖oken讀取循環(huán)處添加一些丑陋的守衛(wèi)。
def parse(str: String) = {
val parser = parserFactory.createJsonParser(str)
var nested = 0
if (parser.nextToken() == START_OBJECT) {
var token = parser.nextToken()
var textOpt:Option[String] = None
var idOpt:Option[Long] = None
while(token != null) {
if (token == FIELD_NAME && nested == 0) {
parser.getCurrentName() match {
case "text" => {
parser.nextToken()
textOpt = Some(parser.getText())
}
case "id" => {
parser.nextToken()
idOpt = Some(parser.getLongValue())
}
case _ => // noop
}
} else if (token == START_OBJECT) {
nested += 1
} else if (token == END_OBJECT) {
nested -= 1
}
token = parser.nextToken()
}
if (textOpt.isDefined && idOpt.isDefined) {
Some(SimpleParsed(idOpt.get, textOpt.get))
} else {
None
}
} else {
None
}
}
…測(cè)試通過(guò)了!
現(xiàn)在我們已經(jīng)可以運(yùn)行package命令來(lái)生成一個(gè)jar文件。不過(guò)我們可能要與其他組分享我們的jar包。要做到這一點(diǎn),我們將在StandardProject基礎(chǔ)上構(gòu)建,這給了我們一個(gè)良好的開(kāi)端。
第一步是引入StandardProject為SBT插件。插件是一種為你的構(gòu)建引進(jìn)依賴的方式,注意不是為你的項(xiàng)目引入。這些依賴關(guān)系定義在project/plugins/Plugins.scala文件中。添加以下代碼到Plugins.scala文件中。
import sbt._
class Plugins(info: ProjectInfo) extends PluginDefinition(info) {
val twitterMaven = "twitter.com" at "http://maven.twttr.com/"
val defaultProject = "com.twitter" % "standard-project" % "0.7.14"
}
注意我們指定了一個(gè)Maven倉(cāng)庫(kù)和一個(gè)依賴。這是因?yàn)檫@個(gè)標(biāo)準(zhǔn)項(xiàng)目庫(kù)是由twitter托管的,不在SBT默認(rèn)檢查的倉(cāng)庫(kù)中。
我們也將更新項(xiàng)目定義來(lái)擴(kuò)展StandardProject,包括SVN發(fā)布特質(zhì),和我們希望發(fā)布的倉(cāng)庫(kù)定義。修改SampleProject.scala
import sbt._
import com.twitter.sbt._
class SampleProject(info: ProjectInfo) extends StandardProject(info) with SubversionPublisher {
val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1"
val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test"
override def subversionRepository = Some("http://svn.local.twitter.com/maven/")
}
現(xiàn)在如果我們運(yùn)行發(fā)布操作,將看到以下輸出
[info] == deliver ==
IvySvn Build-Version: null
IvySvn Build-DateTime: null
[info] :: delivering :: com.twitter#sample;1.0-SNAPSHOT :: 1.0-SNAPSHOT :: release :: Wed Nov 24 10:26:45 PST 2010
[info] delivering ivy file to /Users/mmcbride/projects/sbt-sample/target/ivy-1.0-SNAPSHOT.xml
[info] == deliver ==
[info]
[info] == make-pom ==
[info] Wrote /Users/mmcbride/projects/sbt-sample/target/sample-1.0-SNAPSHOT.pom
[info] == make-pom ==
[info]
[info] == publish ==
[info] :: publishing :: com.twitter#sample
[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar
[info] published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar
[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom
[info] published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom
[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml
[info] published ivy to com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml
[info] Binary diff deleting com/twitter/sample/1.0-SNAPSHOT
[info] Commit finished r977 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010
[info] Copying from com/twitter/sample/.upload to com/twitter/sample/1.0-SNAPSHOT
[info] Binary diff finished : r978 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010
[info] == publish ==
[success] Successful.
[info]
[info] Total time: 4 s, completed Nov 24, 2010 10:26:47 AM
這樣(一段時(shí)間后),就可以在?binaries.local.twitter.com?上看到我們發(fā)布的jar包。
任務(wù)就是Scala函數(shù)。添加一個(gè)任務(wù)最簡(jiǎn)單的方法是,在你的項(xiàng)目定義中引入一個(gè)val定義的任務(wù)方法,如
lazy val print = task {log.info("a test action"); None}
你也可以這樣加上依賴和描述
lazy val print = task {log.info("a test action"); None}.dependsOn(compile) describedAs("prints a line after compile")
刷新項(xiàng)目,并執(zhí)行print操作,我們將看到以下輸出
> print
[info]
[info] == print ==
[info] a test action
[info] == print ==
[success] Successful.
[info]
[info] Total time: 0 s, completed Nov 24, 2010 11:05:12 AM
>
所以它起作用了。如果你只是在一個(gè)項(xiàng)目定義一個(gè)任務(wù)的話,這工作得很好。然而如果你定義的是一個(gè)插件的話,它就很不靈活了。我可能要
lazy val print = printAction
def printAction = printTask.dependsOn(compile) describedAs("prints a line after compile")
def printTask = task {log.info("a test action"); None}
這可以讓消費(fèi)者覆蓋任務(wù)本身,依賴和/或任務(wù)的描述,或動(dòng)作本身。大多數(shù)SBT內(nèi)建的動(dòng)作都遵循這種模式。作為一個(gè)例子,我們可以通過(guò)修改內(nèi)置打包任務(wù)來(lái)打印當(dāng)前時(shí)間戳
lazy val printTimestamp = task { log.info("current time is " + System.currentTimeMillis); None}
override def packageAction = super.packageAction.dependsOn(printTimestamp)
有很多例子介紹了怎樣調(diào)整SBT默認(rèn)的StandardProject,和如何添加自定義任務(wù)。
待續(xù)
Built at?@twitter?by?@stevej,?@marius, and?@lahosken?with much help from?@evanm,?@sprsquish,?@kevino,?@zuercher,?@timtrueman,?@wickman, and@mccv; Russian translation by?appigram; Chinese simple translation by?jasonqu; Korean translation by?enshahar;
Licensed under the?Apache License v2.0.
更多建議: