Scala教程:简单构建工具SBT
这个章节会讲解SBT(Simple Build Tool)!包含的主题有:
- 创建一个sbt工程
- 基本命令
- sbt控制台
- 连续执行命令
- 自定义工程
- 自定义命令
- sbt代码简介(如果时间允许的话)
关于SBT
SBT是一个现代构建工具。它是用Scala编写的,并且针对Scala也提供了很多方便快捷的功能。它也是一个通用的构建工具。
为什么使用SBT?
- 强大的依赖管理功能
- Ivy用来管理依赖
- 一个只会根据需求更新的模型
 
- 所有任务的创建都支持Scala
- 可连续执行命令
- 可以在工程的上下文里启动REPL
开始
- 下载jar包:http://code.google.com/p/simple-build-tool/downloads/list
- 创建一个stb shell脚本来调用jar包,例如:
| 1 | java -Xmx512M -jar sbt-launch.jar "$@" | 
- 保证以上命令能够正确执行,它已经放在了path下
- 运行sbt来创建工程
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | [local ~/projects]$ sbtProject does not exist, create newproject? (y/N/s) yName: sampleOrganization: com.twitterVersion [1.0]: 1.0-SNAPSHOTScala version [2.7.7]: 2.8.1sbt version [0.7.4]:      Getting Scala 2.7.7...:: retrieving :: org.scala-tools.sbt#boot-scala    confs: [default]    2artifacts copied, 0already retrieved (9911kB/221ms)Getting org.scala-tools.sbt sbt_2.7.70.7.4...:: retrieving :: org.scala-tools.sbt#boot-app    confs: [default]    15artifacts copied, 0already retrieved (4096kB/167ms)[success] Successfully initialized directory structure.Getting Scala 2.8.1...:: retrieving :: org.scala-tools.sbt#boot-scala    confs: [default]    2artifacts copied, 0already retrieved (15118kB/386ms)[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1[info]    using sbt.DefaultProject with sbt 0.7.4and Scala 2.7.7> | 
从一个SNAPSHORT版本来开始你的工程是一个不错的方式。
工程结构
- project – 工程定义文件
- project/build/.scala – 主要的工程定义文件
- project/build.properties – 工程,sbt以及scala版本定义
 
- src/main – 你的应用代码放在这里,不同的子目录名称表示不同的编程语言(例如,src/main/scala,src/main/java)
- src/main/resources – 你想添加到jar包里的静态文件(例如日志配置文件)
- lib_managed – 你的工程所依赖的jar文件。会在sbt更新的时候添加到该目录
- target – 最终生成的文件存放的目录(例如,生成的thrift代码,class文件,jar文件)
添加一些代码
我们会创建一个简单的json解析器来解析简单的tweet。添加下面的代码到 src/main/scala/com/twitter/sample/SimpleParser.scala
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | packagecom.twitter.samplecaseclassSimpleParsed(id: Long, text: String)classSimpleParser {  val tweetRegex = "\"id\":(.*),\"text\":\"(.*)\"".r  def parse(str: String) = {    tweetRegex.findFirstMatchIn(str) match {      caseSome(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,例如:
| 1 | sbt test | 
注意,如果一个命令接受参数,你需要给参数加上引号
| 1 | sbt 'test-only com.twitter.sample.SampleSpec' | 
这种方式很古怪。
暂时不管它。现在启动sbt来构建我们的代码:
| 1 2 3 4 | [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.4and Scala 2.7.7> | 
SBT允许你启动会在你启动Scala REPL的时候加载所有的依赖。它会在启动控制台前先编译工程代码,这样更加便于我们测试我们的解析器了。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | > console[info] [info] == compile ==[info]   Source analysis: 0new/modified, 0indirectly invalidated, 0removed.[info] Compiling main sources...[info] Nothing to compile.[info]   Post-analysis: 3classes.[info] == compile ==[info] [info] == copy-test-resources ==[info] == copy-test-resources ==[info] [info] == test-compile ==[info]   Source analysis: 0new/modified, 0indirectly invalidated, 0removed.[info] Compiling test sources...[info] Nothing to compile.[info]   Post-analysis: 0classes.[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 formore information.scala> | 
代码编译完成,并且提供了经典的Scala命令行提示符。我们会创建一个新的解析器,一个示例的tweet,并且保证它能够正常“工作”。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | scala> importcom.twitter.sample._            importcom.twitter.sample._scala> val tweet = """{"id":1,"text":"foo"}"""tweet: java.lang.String = {"id":1,"text":"foo"}scala> val parser = newSimpleParser          parser: com.twitter.sample.SimpleParser = com.twitter.sample.SimpleParser@71060c3escala> parser.parse(tweet)                    res0: Option[com.twitter.sample.SimpleParsed] = Some(SimpleParsed(1,"foo"}))scala> | 
添加依赖
这个简单的解析器对于这点输入内容是可以正常工作的,但是我们还需要加入测试代码并且对它进行一些改造。首先要做的就是把specs测试库以及一个 真正的JSON解析器加入到我们的工程里来。为了达到这个目标,我们需要在默认的工程结构上进行改造,然后创建项目。把下面的内容添加到 project/build/SampleProject.scala里:
| 1 2 3 4 5 6 | importsbt._classSampleProject(info: ProjectInfo) extendsDefaultProject(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类。
这里你可以通过一个常量来指定具体的依赖。SBT使用在构建期通过反射来扫描你工程里所有的依赖常量,并且生成一个依赖树。这个语法可能比较新,不过它和下面的maven依赖是等同的:
| 1 2 3 4 5 6 7 8 | org.codehaus.jacksonjackson-core-asl1.6.1org.scala-tools.testingspecs_2.8.01.6.5test | 
现在我们可以把依赖的库下载下来了。从命令行(而不是sbt控制台)里运行sbt update
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [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.4and 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]  1artifacts copied, 0already retrieved (2785kB/71ms)[info] == update ==[success] Successful.[info] [info] Total time: 1s, completed Nov 24, 20108:47:26AM[info] [info] Total session time: 2s, completed Nov 24, 20108:47:26AM[success] Build completed successfully. | 
你会看到sbt解析出了specs库。 你的工程下面现在有了libmanaged目录,并且在libmanaged/scala2.8.1/test目录下会有specs2.8.0-1.6.5.jar文件。
添加测试用例
现在我们添加了测试二方库,把下面的代码添加到src/test/scala/com/twitter/sample/SimpleParserSpec.scala里
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | packagecom.twitter.sampleimportorg.specs._object SimpleParserSpec extendsSpecification {  "SimpleParser"should {    val parser = newSimpleParser()    "work with basic tweet"in {      val tweet = """{"id":1,"text":"foo"}"""      parser.parse(tweet) match {        caseSome(parsed) => {          parsed.text must be_==("foo")          parsed.id must be_==(1)        }        case_ => fail("didn't parse tweet")      }    }  }} | 
在sbt控制台里,运行test命令
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | > test[info] [info] == compile ==[info]   Source analysis: 0new/modified, 0indirectly invalidated, 0removed.[info] Compiling main sources...[info] Nothing to compile.[info]   Post-analysis: 3classes.[info] == compile ==[info] [info] == test-compile ==[info]   Source analysis: 0new/modified, 0indirectly invalidated, 0removed.[info] Compiling test sources...[info] Nothing to compile.[info]   Post-analysis: 10classes.[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: 0s, completed Nov 24, 20108:54:45AM> | 
我们的测试用例执行了!现在我们可以添加更多的测试用例。SBT提供的一个很好的功能就是自动运行被触发的动作。它会预先启动一个循环,不断检测代码改动,一旦有改动就执行相应的动作。我们来运行~test命令,看看有什么效果。
| 1 2 3 4 5 | [info] == test ==[success] Successful.[info] [info] Total time: 0s, completed Nov 24, 20108:55:50AM1. Waiting forsource changes... (press enter to interrupt) | 
现在,我们添加下面的测试用例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | "reject a non-JSON tweet"in {  val tweet = """"id":1,"text":"foo""""  parser.parse(tweet) match {    caseSome(parsed) => fail("didn't reject a non-JSON tweet")    casee => e must be_==(None)  }}"ignore nested content"in {  val tweet = """{"id":1,"text":"foo","nested":{"id":2}}"""  parser.parse(tweet) match {    caseSome(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 {    caseSome(parsed) => fail("didn't reject a partial tweet")    casee => e must be_==(None)  }} | 
一旦我们保存好文件,SBT会检测到改动,它会运行测试代码,并且告诉我们parser的实现有问题。
| 1 2 3 4 5 6 7 8 9 | [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 | 
那么我们就来重构JSON parser的代码,让它更接近真实的parser。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | packagecom.twitter.sampleimportorg.codehaus.jackson._importorg.codehaus.jackson.JsonToken._caseclassSimpleParsed(id: Long, text: String)classSimpleParser {  val parserFactory = newJsonFactory()  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    }  }} | 
这是一个简单的Json解析器。当我们保存代码时,SBT会编译我们的代码并且运行测试代码。越来越方便了!
| 1 2 3 4 5 6 7 | 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 == | 
噢。我们需要考虑嵌套的对象。我们来给读入token的循环加上恶心的处理代码。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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        }      } elseif(token == START_OBJECT) {        nested += 1      } elseif(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文件。
| 1 2 3 4 5 6 | importsbt._classPlugins(info: ProjectInfo) extendsPluginDefinition(info) {  val twitterMaven = "twitter.com"at "http://maven.twttr.com/"  val defaultProject = "com.twitter"% "standard-project"% "0.7.14"} | 
注意我们把一个maven仓库也作为依赖添加进来。这是因为这个标准项目库是我们维护的,而不是在sbt的默认仓库里。
同时,我们也需要更新我们的工程定义,让他扩展StandProject类,还需要扩展一个SubversionPublisher trait,同时也需要定义我们打算发布的仓库。把SampleProject.scala修改成如下所示:
| 1 2 3 4 5 6 7 8 9 | importsbt._importcom.twitter.sbt._classSampleProject(info: ProjectInfo) extendsStandardProject(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/")} | 
现在我们来执行publish的动作,会看到下面的结果
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | [info] == deliver ==IvySvn Build-Version: nullIvySvn Build-DateTime: null[info] :: delivering :: com.twitter#sample;1.0-SNAPSHOT :: 1.0-SNAPSHOT :: release :: Wed Nov 2410:26:45PST 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 2410:26:47PST 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 2410:26:47PST 2010[info] == publish ==[success] Successful.[info] [info] Total time: 4s, completed Nov 24, 201010:26:47AM | 
然后(过一段时间),我们可以在binaries.local.twitter.com:http://binaries.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/下面看到我们发布的jar包。
添加任务
任务都是Scala函数。添加任务最简单的方式是包含一个通过task方法定义的常量,例如:
| 1 | lazy val print = task {log.info("a test action"); None} | 
如果你要添加依赖,同时添加一个描述,你可以按照下面的方式来添加:
| 1 | lazy val print = task {log.info("a test action"); None}.dependsOn(compile) describedAs("prints a line after compile") | 
如果我们重新加载工程,然后运行print动作,就会看到下面的内容:
| 1 2 3 4 5 6 7 8 9 | > print[info] [info] == print ==[info] a test action[info] == print ==[success] Successful.[info] [info] Total time: 0s, completed Nov 24, 201011:05:12AM> | 
这样确实可以的。如果你在一个单一的工程里创建这样的任务是没问题的。但是如果你在插件里这样定义的话,是非常不灵活的。我可能这样做
| 1 2 3 | lazy val print = printActiondef printAction = printTask.dependsOn(compile) describedAs("prints a line after compile")def printTask = task {log.info("a test action"); None} | 
这样使得用户可以自己重写任务,依赖,任务的描述,或是任务的行为。SBT大部分的内置行为都是这种模式的。作为范例,我们可以修改内置的package任务,让他在做下面的任务时打印出时间戳。
| 1 2 | lazy val printTimestamp = task { log.info("current time is "+ System.currentTimeMillis); None}override def packageAction = super.packageAction.dependsOn(printTimestamp) | 
在StandarProject里有很多对于SBT默认配置的调整和自定义任务的示例。
速查手册
常用命令
- actions – 显示对当前工程可用的命令
- update – 下载依赖
- compile – 编译代码
- test – 运行测试代码
- package – 创建一个可发布的jar包
- publish-local – 把构建出来的jar包安装到本地的ivy缓存
- publish – 把jar包发布到远程仓库(如果配置了的话)
更多命令
- test-failed – 运行失败的spec
- test-quick – 运行所有失败的以及/或者是由依赖更新的spec
- clean-cache – 清除所有的sbt缓存。类似于sbt的clean命令
- clean-lib – 删除lib_managed下的所有内容
工程结构
未完待续
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号