SBT 是一个现代化的构建工具。虽然它由 Scala 编写并提供了很多 Scala 便利,但它是一个通用的构建工具。
为什么选择 SBT?
译注:最新的 SBT 安装方式请参考 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 |
可以看到它已经以较好的形式创建了项目的快照版本。
project/build/.scala
– 主项目定义文件
project/build.properties
– 项目、sbt 和 Scala 版本定义
src/main
– 你的应用程序代码出现在这里,在子目录表明代码的语言(如src/main/scala
, src/main/java
)
src/main/resources
– 你想要添加到 jar 包中的静态文件(如日志配置)
src/test
– 就像 src/main,不过是对测试
lib_managed
– 你的项目依赖的 jar文件。由 sbt update 时填充
target
– 生成物的目标路径(如自动生成的 thrift 代码,类文件,jar包)
我们将为简单的 tweet 消息创建一个简单的 JSON 解析器。将以下代码加在这个文件中 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,但应该能够编译通过。
SBT 既可以用作命令行脚本,也可以作为构建控制台。我们将主要利用它作为构建控制台,不过大多数命令可以作为参数传递给 SBT 独立运行,如
sbt test
需要注意如果一个命令需要参数,你需要使用引号包括住整个参数路径,例如
sbt 'test-only com.twitter.sample.SampleSpec'
这种方式很奇怪。
不管怎样,要开始我们的代码工作了,启动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 |
> |
> 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> |
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> |
我们简单的解析器对这个非常小的输入集工作正常,但我们需要添加更多的测试并让它出错。第一步是在我们的项目中添加 specs 测试库和一个真正的 JSON 解析器。要做到这一点,我们必须超越默认的 SBT 项目布局来创建一个项目。
SBT 认为 project/build
目录中的 Scala 文件是项目定义。添加以下内容到这个文件中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" |
} |
一个项目定义是一个 SBT 类。在上面例子中,我们扩展了 SBT 的 DefaultProject。
这里是通过 val 声明依赖。SBT 使用反射来扫描项目中的所有 val 依赖,并在构建时建立依赖关系树。这里使用的语法可能是新的,但本质和 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> |
[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. |
你会看到 sbt 检索到 specs 库。现在还增加了一个 lib_managed 目录,并且在 lib_managed/scala_2.8.1/test
目录中包含 specs_2.8.0-1.6.5.jar
现在有了测试库,可以把下面的测试代码写入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") |
} |
} |
} |
} |
> 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 |
> |
~test
并看看会发生什么吧。
[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) |
"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) |
} |
} |
[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 |
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 |
} |
} |
} |
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 == |
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 |
} |
} |
…测试通过了!
现在我们已经可以运行 package 命令来生成一个 jar 文件。不过我们可能要与其他组分享我们的 jar 包。要做到这一点,我们将在 StandardProject 基础上构建,这给了我们一个良好的开端。
第一步是引入 StandardProject 为 SBT 插件。插件是一种为你的构建引进依赖的方式,注意不是为你的项目引入。这些依赖关系定义在 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" |
} |
我们也将更新项目定义来扩展 StandardProject,包括 SVN 发布特质,和我们希望发布的仓库定义。修改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/") |
} |
[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 |
这样(一段时间后),就可以在 binaries.local.twitter.com
上看到我们发布的 jar 包。
任务就是 Scala 函数。添加一个任务最简单的方法是,在你的项目定义中引入一个 val 定义的任务方法,如
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") |
[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 |
> |
lazy val print = printAction |
def printAction = printTask.dependsOn(compile) describedAs("prints a line after compile") |
def printTask = task {log.info("a test action"); None} |
常用命令
更多命令