簡單構(gòu)建工具

2022-05-13 10:37 更新

關(guān)于 SBT

SBT 是一個現(xiàn)代化的構(gòu)建工具。雖然它由 Scala 編寫并提供了很多 Scala 便利,但它是一個通用的構(gòu)建工具。

為什么選擇 SBT?

  • 明智的依賴管理
  • 使用 Ivy 做依賴管理
  • “只在請求時更新”的模型
  • 對創(chuàng)建任務(wù)全面的 Scala 語言支持
  • 連續(xù)執(zhí)行命令
  • 在項目上下文內(nèi)啟動解釋器

入門

譯注:最新的 SBT 安裝方式請參考 scala-sbt 的文檔

  • 下載 jar 包地址
  • 創(chuàng)建一個調(diào)用這個 jar 的 SBT shell 腳本,例如
java -Xmx512M -jar sbt-launch.jar "$@"
  • 確保它是可執(zhí)行的,并在你的 path 下
  • 運行 sbt 來創(chuàng)建項目
[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)建了項目的快照版本。

項目布局

  • 項目 – 項目定義文件
    • project/build/.scala – 主項目定義文件
    • project/build.properties – 項目、sbt 和 Scala 版本定義
  • src/main – 你的應(yīng)用程序代碼出現(xiàn)在這里,在子目錄表明代碼的語言(如src/main/scala, src/main/java
  • src/main/resources – 你想要添加到 jar 包中的靜態(tài)文件(如日志配置)
  • src/test – 就像 src/main,不過是對測試
  • lib_managed – 你的項目依賴的 jar文件。由 sbt update 時填充
  • target – 生成物的目標路徑(如自動生成的 thrift 代碼,類文件,jar包)

添加一些代碼

我們將為簡單的 tweet 消息創(chuàng)建一個簡單的 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,但應(yīng)該能夠編譯通過。

在控制臺中的測試

SBT 既可以用作命令行腳本,也可以作為構(gòu)建控制臺。我們將主要利用它作為構(gòu)建控制臺,不過大多數(shù)命令可以作為參數(shù)傳遞給 SBT 獨立運行,如

sbt test

需要注意如果一個命令需要參數(shù),你需要使用引號包括住整個參數(shù)路徑,例如

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
> 

SBT 允許你啟動一個 Scala REPL 并加載所有項目依賴。它會在啟動控制臺前編譯項目的源代碼,從而為我們提供一個快速測試解析器的工作臺。

> 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 提示符。我們將創(chuàng)建一個新的解析器,一個 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> 

添加依賴

我們簡單的解析器對這個非常小的輸入集工作正常,但我們需要添加更多的測試并讓它出錯。第一步是在我們的項目中添加 specs 測試庫和一個真正的 JSON 解析器。要做到這一點,我們必須超越默認的 SBT 項目布局來創(chuàng)建一個項目。

SBT 認為 project/build 目錄中的 Scala 文件是項目定義。添加以下內(nèi)容到這個文件中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 依賴,并在構(gòu)建時建立依賴關(guān)系樹。這里使用的語法可能是新的,但本質(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)在可以下載我們的項目依賴了。在命令行中(而不是 sbt console 中)運行 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.

你會看到 sbt 檢索到 specs 庫?,F(xiàn)在還增加了一個 lib_managed 目錄,并且在 lib_managed/scala_2.8.1/test目錄中包含 specs_2.8.0-1.6.5.jar

添加測試

現(xiàn)在有了測試庫,可以把下面的測試代碼寫入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 控制臺中運行 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
> 

我們的測試通過了!現(xiàn)在,我們可以增加更多。運行觸發(fā)動作是 SBT 提供的優(yōu)秀特性之一。在動作開始添加一個波浪線會啟動一個循環(huán),在源文件發(fā)生變化時重新運行動作。讓我們運行 ~test 并看看會發(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)在,讓我們添加下面的測試案例

 "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 會檢測到變化,運行測試,并通知我們的解析器有問題

[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

因此,讓我們返工實現(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
    }
  }
}

這是一個簡單的 Jackson 解析器。當我們保存,SBT 會重新編譯代碼和運行測試。代碼變得越來越好了!

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 讀取循環(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
    }
  }

…測試通過了!

打包和發(fā)布

現(xiàn)在我們已經(jīng)可以運行 package 命令來生成一個 jar 文件。不過我們可能要與其他組分享我們的 jar 包。要做到這一點,我們將在 StandardProject 基礎(chǔ)上構(gòu)建,這給了我們一個良好的開端。

第一步是引入 StandardProject 為 SBT 插件。插件是一種為你的構(gòu)建引進依賴的方式,注意不是為你的項目引入。這些依賴關(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"
}

注意我們指定了一個 Maven 倉庫和一個依賴。這是因為這個標準項目庫是由 twitter 托管的,不在 SBT 默認檢查的倉庫中。

我們也將更新項目定義來擴展 StandardProject,包括 SVN 發(fā)布特質(zhì),和我們希望發(fā)布的倉庫定義。修改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)在如果我們運行發(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

這樣(一段時間后),就可以在 binaries.local.twitter.com 上看到我們發(fā)布的 jar 包。

添加任務(wù)

任務(wù)就是 Scala 函數(shù)。添加一個任務(wù)最簡單的方法是,在你的項目定義中引入一個 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")

刷新項目,并執(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
> 

所以它起作用了。如果你只是在一個項目定義一個任務(wù)的話,這工作得很好。然而如果你定義的是一個插件的話,它就很不靈活了。我可能要

lazy val print = printAction
def printAction = printTask.dependsOn(compile) describedAs("prints a line after compile")
def printTask = task {log.info("a test action"); None}

這可以讓消費者覆蓋任務(wù)本身,依賴和/或任務(wù)的描述,或動作本身。大多數(shù) SBT 內(nèi)建的動作都遵循這種模式。作為一個例子,我們可以通過修改內(nèi)置打包任務(wù)來打印當前時間戳

lazy val printTimestamp = task { log.info("current time is " + System.currentTimeMillis); None}
override def packageAction = super.packageAction.dependsOn(printTimestamp)

有很多例子介紹了怎樣調(diào)整 SBT 默認的 StandardProject,和如何添加自定義任務(wù)。

快速參考

常用命令

  • actions – 顯示這個項目中可用的動作
  • update – 下載依賴
  • compile – 編譯源文件
  • test – 運行測試
  • package – 創(chuàng)建一個可發(fā)布的 jar 文件
  • publish-local – 在本地 ivy 緩存中安裝構(gòu)建好的jar包
  • publish – 將你的 jar 推到一個遠程庫中(如果配置了的話)

更多命令

  • test-failed – 運行所有失敗的規(guī)格測試
  • test-quick – 運行任何失敗的和/或依賴更新的規(guī)格
  • clean-cache – 刪除 SBT 緩存各種的東西。就像 sbt 的 clean 命令
  • clean-lib – 刪除 lib_managed 下的一切
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號