一個簡單的build.sbt文件內容以下:html
name := "hello" // 項目名稱 organization := "xxx.xxx.xxx" // 組織名稱 version := "0.0.1-SNAPSHOT" // 版本號 scalaVersion := "2.9.2" // 使用的Scala版本號 // 其它build定義
其中, name和version的定義是必須的,由於若是想生成jar包的話,這兩個屬性的值將做爲jar包名稱的一部分。java
build.sbt的內容其實很好理解,能夠簡單理解爲一行表明一個鍵值對(Key-Value Pair),各行之間以空行相分割。git
固然,實際狀況要比這複雜,須要理解SBT的Settings引擎才能夠徹底領會, 以上原則只是爲了便於讀者理解build.sbt的內容。github
除了定義以上項目相關信息,咱們還能夠在build.sbt中添加項目依賴web
// 添加源代碼編譯或者運行期間使用的依賴 libraryDependencies += "ch.qos.logback" % "logback-core" % "1.0.0" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.0.0" // 或者 libraryDependencies ++= Seq( "ch.qos.logback" % "logback-core" % "1.0.0", "ch.qos.logback" % "logback-classic" % "1.0.0", ... ) // 添加測試代碼編譯或者運行期間使用的依賴 libraryDependencies ++= Seq("org.scalatest" %% "scalatest" % "1.8" % "test")
甚至於直接使用ivy的xml定義格式:shell
ivyXML := <dependencies> <dependency org="org.eclipse.jetty.orbit" name="javax.servlet" rev="3.0.0.v201112011016"> <artifact name="javax.servlet" type="orbit" ext="jar"/> </dependency> <exclude module="junit"/> <exclude module="activation"/> <exclude module="jmxri"/> <exclude module="jmxtools"/> <exclude module="jms"/> <exclude module="mail"/> </dependencies>
在這裏,咱們排除了某些沒必要要的依賴,而且聲明瞭某個定製過的依賴聲明。apache
固然, build.sbt文件中還能夠定義不少東西,好比添加插件,聲明額外的repository,聲明各類編譯參數等等,咱們這裏就不在一一贅述了。緩存
project目錄下的幾個文件實際上都是非必須存在的,能夠根據狀況添加。架構
build.properties 文件聲明使用的要使用哪一個版本的SBT來編譯當前項目, 最新的sbt boot launcher能夠可以兼容編譯全部0.10.x版本的SBT構建項目,好比若是我使用的是0.12版本的sbt,但卻想用0.11.3版本的sbt來編譯當前項目,則能夠在build.properties文件中添加 sbt.version=0.11.3
來指定。 默認狀況下,當前項目的構建採用使用的sbt boot launcher對應的版本。併發
plugins.sbt 文件用來聲明當前項目但願使用哪些插件來加強當前項目使用的sbt的功能,好比像assembly功能,清理ivy local cache功能,都有相應的sbt插件供使用, 要使用這些插件只須要在plugins.sbt中聲明便可,不用本身去再造輪子:
resolvers += Resolver.url("git://github.com/jrudolph/sbt-dependency-graph.git") resolvers += "sbt-idea-repo" at "http://mpeltonen.github.com/maven/" addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.1.0") addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.6.0")
在筆者的項目中, 使用sbt-idea來生成IDEA IDE對應的meta目錄和文件,以便可以使用IDEA來編寫項目代碼; 使用sbt-dependency-graph來發現項目使用的各個依賴之間的關係;
爲了可以成功加載這些sbt插件,咱們將他們的查找位置添加到resolovers當中。有關resolvers的內容,咱們後面將會詳細介紹,這裏注意一個比較有趣的地方就是,sbt支持直接將相應的github項目做爲依賴或者插件依賴,而不用非得先將相應的依賴或者插件發佈到maven或者ivy的repository當中纔可使用。
以上目錄和文件一般是在建立項目的時候須要咱們建立的,實際上, SBT還會在編譯或者運行期間自動生成某些相應的目錄和文件,好比SBT會在項目的根目錄下和project目錄下自動生成相應的target目錄,並將編譯結果或者某些緩存的信息置於其中, 通常狀況下,咱們不但願將這些目錄和文件記錄到版本控制系統中,因此,一般會將這些目錄和文件排除在版本管理以外。
好比, 若是咱們使用git來作版本控制,那麼就能夠在.gitignore中添加一行 "target/"
來排除項目根目錄下和project目錄下的target目錄及其相關文件。
sbt的managed dependencies採用Apache Ivy的依賴管理方式, 能夠支持從Maven或者Ivy的Repository中自動下載相應的依賴。
簡單來講,在SBT中, 使用managed dependencies基本上就意味着往__libraryDependencies__這個Key中添加所須要的依賴, 添加的通常格式以下:
libraryDependencies += groupID % artifactID % revision
好比:
libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3"
這種格式實際上是簡化的常見形式,實際上,咱們還能夠作更多微調, 好比:
(1) libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" % "test" (2) libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" exclude("org", "artifact") (3) libraryDependencies += "org.apache.derby" %% "derby" % "10.4.1.3"
(1)的形式容許咱們限定依賴的範圍只限於測試期間; (2)的形勢容許咱們排除遞歸依賴中某些咱們須要排除的依賴; (3)的形式則會在依賴查找的時候,將當前項目使用的scala版本號追加到artifactId以後做爲完整的artifactId來查找依賴,好比若是咱們的項目使用scala2.9.2,那麼(3)的依賴聲明實際上等同於 "org.apache.derby" %% "derby_2.9.2" % "10.4.1.3"
,這種方式更可能是爲了簡化同一依賴類庫存在有多個Scala版本對應的發佈的狀況。
若是有一堆依賴要添加,一行一行的添加是一種方式,其實也能夠一次添加多個依賴:
libraryDependencies ++= Seq("org.apache.derby" %% "derby" % "10.4.1.3", "org.scala-tools" %% "scala-stm" % "0.3", ...)
對於managed dependencies來講,雖然咱們指定了依賴哪些類庫,但有沒有想過,SBT是如何知道到哪裏去抓取這些類庫和相關資料那?!
實際上,默認狀況下, SBT回去默認的Maven2的Repository中抓取依賴,但若是默認的Repository中找不到咱們的依賴,那咱們能夠經過resolver機制,追加更多的repository讓SBT去查找並抓取, 好比:
resolvers += "Sonatype OSS Snapshots" at " https://oss.sonatype.org/content/repositories/snapshots "
at^[at其實是String類型進行了隱式類型轉換(Implicit conversion)後目標類型的方法名]以前是要追加的repository的標誌名稱(任意取),at後面則是要追加的repository的路徑。
除了可遠程訪問的Maven Repo,咱們也能夠將本地的Maven Repo追加到resolver的搜索範圍:
resolvers += "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository"
對於簡單的項目來說,.sbt形式的build定義文件就能夠知足須要了,但若是咱們想要使用SBT的一些高級特性,好比自定義Task, 多模塊的項目構建, 就必須使用.scala形式的build定義了。 簡單來說,.sbt能幹的事情,.scala形式的build定義都能幹,反之,則否則。
要使用.scala形式的build定義,只要在當前項目根目錄下的project/子目錄下新建一個.scala後綴名的scala源代碼文件便可,好比Build.scala(名稱能夠任意,通常使用Build.scala):
import sbt._ import Keys._ object HelloBuild extends Build { override lazy val settings = super.settings ++ Seq(..) lazy val root = Project(id = "hello", base = file("."), settings = Project.defaultSettings ++ Seq(..)) }
build的定義只要擴展sbt.Build,而後添加相應的邏輯便可,全部代碼都是標準的scala代碼,在Build定義中,咱們能夠添加更多的settings, 添加自定義的task,添加相應的val和方法定義等等, 更多代碼實例能夠參考SBT Wiki( https://github.com/harrah/xsbt/wiki/Examples ),另外,咱們在後面介紹SBT的更多高級特性的時候,也會引入更多.scala形式的build定義的使用。
NOTE .sbt和.scala之間不得不說的那些事兒 實際上, 兩種形式並不排斥,並非說我使用了前者,就不能使用後者,對於某些單一的項目來講,咱們能夠在.sbt中定義經常使用的settings,而在.scala中定義自定義的其它內容, SBT在編譯期間,會將.sbt中的settings等定義與.scala中的定義合併,做爲最終的build定義使用。 只有在多模塊項目構建中,爲了不多個.sbt的存在引入過多的繁瑣,纔會只用.scala形式的build定義。 .sbt和.scala兩者之間的settings是可互相訪問的, .scala中的內容會被import到.sbt中,而.sbt中的settings也會被添加到.scala的settings當中。默認狀況下,.sbt中的settings會被歸入Project級別的Scope中,除非明確指定哪些Settings定義的Scope; .scala中則能夠將settings歸入Build級別的Scope,也能夠歸入Project級別的Scope。
在瞭解了.sbt和.scala兩種形式的build定義形式以後, 咱們就能夠來看看SBT項目構建結構的本質了。
首先, 一個SBT項目,與構建相關聯的基本設施能夠概況爲3個部分, 即:
也就是說, 對於一個SBT項目來講,SBT在構建的時候,只關心兩點:
在以上基礎規則的約束下,咱們來引入一個推導條件, 即:
項目的根目錄下的project/目錄,其自己也是一個標準的SBT項目。
在這個條件下,咱們再來仔細分析hello/project/目錄,看它目錄下的各項artifacts到底本質上應該是什麼。
咱們說項目根目錄下的project/子目錄下的*.scala文件是當前項目的build定義文件, 而根據以上的推導條件, project/目錄自己又是一個SBT項目,咱們還知道,SBT下面下的*.scala都是當前項目的源代碼,因此project/下的*.scala文件, 其實都是project這個目錄下的SBT項目的源代碼,而這些源代碼中,若是有人定義了sbt.Build,那麼就會被用做project目錄上層目錄界定的SBT項目的build定義文件, right?!
那麼,來想一個問題,若是project/目錄下的*.scala是源代碼文件,而project目錄總體又是一個標準的SBT項目, 假如咱們這些*.scala源碼文件中須要依賴其餘三方庫,一般咱們會怎麼作?
對, 在當前項目的根目錄下新建一個build.sbt文件,將依賴添加進去,因此,咱們就有了以下的項目結構:
hello/ *.scala build.sbt project/ *.scala build.sbt
也就是說,咱們能夠在書寫當前項目的build定義的時候(由於build定義也是用scala來寫),借用第三方依賴來完成某些工做,而不用什麼都從新去寫,在project/build.sbt下添加項目依賴,那麼就能夠在project/*.scala裏面使用,進而構建出hello/項目的build定義是什麼, 即hello/project/這個SBT項目,支撐了上一層hello/這個項目的構建!
如今再來想一下,若是hello/project/這個項目的構建要用到其它SBT特性,好比自定義task或者command啥的,咱們該怎麼辦?!
既然hello/project/也是一個SBT項目,那麼按照慣例,咱們就能夠再其下再新建一個project/目錄,在這個下一層的project/目錄下再添加*.scala源文件做爲hello/project/這個SBT項目的build定義文件, 整個項目又變成了:
hello/ *.scala build.sbt project/ *.scala build.sbt /project *.scala
而若是hello/project/project/下的源碼又要依賴其餘三方庫那?! God, 再添加*.sbt或更深一層的project/*.scala!
也就是說, 從第一層的項目根目錄開始, 其下project/目錄內部再嵌套project/目錄,能夠無限遞歸,並且每一層的project/目錄都界定了一個SBT項目,而每個下層的project目錄界定的SBT項目其實都是對上一層的SBT項目作支持,做爲上一層SBT項目的build定義項目,這就跟俄羅斯娃娃這種玩具似的, 遞歸嵌套,一層又包一層:
通常狀況下,咱們不會搞這麼多嵌套,但理解了SBT項目的這個結構上的本質,能夠幫助咱們更好的理解後面的內容,若是讀者看一遍沒能理解,那不妨多看幾回,多參考其餘資料,多揣摩揣摩吧!
大部分狀況下,咱們都是使用SBT內建的Task,好比compile, run等,實際上, 除了這些,咱們還能夠在build定義中添加更多自定義的Task。
自定義SBT的Task其實很簡單,就跟把大象關冰箱裏同樣簡單, 概況來講其實就是:
Task的定義分兩部分,第一部分就是要定義一個TaskKey來標誌Task, 第二部分則是定義Task的執行邏輯。
假設咱們要定義一個簡單的打印"hello, sbt~"信息的task,那第一步就是先定義它的Key,以下代碼所示:
val hello = TaskKey[Unit]("hello", "just say hello")
TaskKey的類型指定了對應task的執行結果,由於咱們只想打印一個字符串,不須要返回什麼數據,因此定義的是TaskKey[Unit]。 定義TaskKey最主要的一點就是要指定一個名稱(好比第一個參數「hello」),這個名稱將是咱們調用該task的標誌性建築。 另外,還能夠可選擇的經過第二個參數傳入該task的相應描述和說明。
有了task對應的Key以後,咱們就要定義task對應的執行邏輯,並經過 :=
方法將相應的key和執行邏輯定義關聯到一塊兒:
hello := { println("hello, sbt~") }
完整的task定義代碼以下所示:
val hello = TaskKey[Unit]("hello", "just say hello") hello := { println("hello, sbt~") }
NOTE := 只是簡單的將task的執行邏輯和key關聯到一塊兒, 若是以前已經將某一執行邏輯跟同一key關聯過,則後者將覆蓋前者,另外,若是咱們想要服用其餘的task的執行邏輯,或者依賴其餘task,只有一個:=就有些力不從心了。這些狀況下,能夠考慮使用~=或者<<=等方法,他們能夠藉助以前的task來映射或者轉換新的task定義。好比(摘自sbt wiki): // These two settings are equivalent intTask <<= intTask map { (value: Int) => value + 1 } intTask ~= { (value: Int) => value + 1 }
光完成了task的Key和執行邏輯定義還不夠,咱們要將這個task添加到項目的Settings當中才能使用它,因此,咱們稍微對以前的代碼作一補充:
object ProjectBuild extends Build { val hello = TaskKey[Unit]("hello", "just say hello") val helloTaskSetting = hello := { println("hello, sbt~") } lazy val root = Project(id = "", base = file(".")).settings(Defaults.defaultSettings ++ Seq(helloTaskSetting): _*) }
將Key與task的執行邏輯相關聯的過程其實是構建某個Setting的過程,雖然咱們也能夠將以上定義寫成以下形式:
lazy val root = Project(id = "", base = file(".")).settings(Defaults.defaultSettings ++ Seq(hello := { println("hello, sbt~") }): _*)
但未免代碼就太不雅觀,也很差管理了(若是要添加多個自定義task,想一想,用這種形式是否是會讓代碼醜陋不堪那?!),因此,咱們引入了helloTaskSetting這個標誌常量來幫助咱們淨化代碼結構 :)
萬事俱備以後,就可使用咱們的自定義task了,使用定義Key的時候指定的task名稱來調用它便可:
$ sbt hello hello, sbt~ // 或者 $ sbt > hello hello, sbt~ [success] Total time: 0 s, completed Oct 4, 2012 2:48:48 PM
怎麼樣? 在SBT中自定義task是否是很簡單那?!
每一個項目最終都要以相應的形式發佈^[這裏的發佈更可能是指特殊的發佈形式,好比提供完整的下載包給用戶,直接打包成部署包等。通常狀況下,若是用Maven或者SBT,能夠直接publish到相應的Maven或者Ivy Repository中],好比二進制包, 源碼包,甚至直接可用的部署包等等, 假設咱們想把當前的SBT項目打包成可直接解壓部署的形式,咱們可使用剛剛介紹的自定義task來完成這一工做:
object ProjectBuild extends Build { import Tasks._ lazy val root = Project(id = "", base = file(".")).settings(Defaults.defaultSettings ++ Seq(distTask, helloTaskSetting): _*) } object Tasks { val hello = TaskKey[Unit]("hello", "just say hello") val helloTaskSetting = hello := { println("hello, sbt~") } val dist = TaskKey[Unit]("dist", "distribute current project as zip or gz packages") val distTask = dist <<= (baseDirectory, target, fullClasspath in Compile, packageBin in Compile, resources in Compile, streams) map { (baseDir, targetDir, cp, jar, res, s) => s.log.info("[dist] prepare distribution folders...") val assemblyDir = targetDir / "dist" val confDir = assemblyDir / "conf" val libDir = assemblyDir / "lib" val binDir = assemblyDir / "bin" Array(assemblyDir, confDir, libDir, binDir).foreach(IO.createDirectory) s.log.info("[dist] copy jar artifact to lib...") IO.copyFile(jar, libDir / jar.name) s.log.info("[dist] copy 3rd party dependencies to lib...") cp.files.foreach(f => if (f.isFile) IO.copyFile(f, libDir / f.name)) s.log.info("[dist] copy shell scripts to bin...") ((baseDir / "bin") ** "*.sh").get.foreach(f => IO.copyFile(