简介
Sbt由两个部分组成,task和setting.
- tasks: Sbt围绕task进行构建.跟Maven不同的是,没有阶段,目标,执行,而只是task.想做一些事情的生活只需要执行task.如果想要一个task在一个task之后执行,只需要在两个task之间添加隐式依赖.如果需要在一个task中使用另一个task的结果,可以将task的输出推送到另一个task,task的结果会在依赖他的task中自动可见.Sbt中task的输出是一个值,可以是任何类型,因此很易于传递.多个task可以依赖于同一个task的结果.默认情况下sbt会以并行的方式运行这些task,同时会使用依赖树,以此来决定哪些顺序执行,哪些并行执行.
- settings: 一个setting在sbt中是一个值,可以是项目的名字或者scala的版本.
Sbt所提供的特性:
- Reproducible builds: 并不关心谁来构建.
- Minimal configuration: 更快的配置并运行.
- Encouragement of good practices: 比如在打包之间运行测试.
- Portability: 可以在任何机器上运行.
- A good ecosystem: 开发者需要做的很少.
同时提供了交互式的环境,会提高开发者的生产力提供了一下特性:
- Faster compilation times: 更少的启动时间,增量编译
- Easy addition of custom tasks: 简便的构建定义
- Faster compile/test cycles through the use of the
~
command: 快速反馈 - The ability to explore your project using the Scala REPL: 快速反馈
搭建
Sbt的搭建只需要下载响应的版本并设置对应的环境变量,版本一般选择最新以避免Bug,环境变量的设置:
$ vim ~/.bash_profile
SBT_HOME=/path/to/your/zip/extraction
PATH=$PATH:$SBT_HOME/bin
export PATH
$ source ~/.bash_profile
然后就可以在终端运行sbt:
$ sbt
>
会看到一些输出信息.
在一个项目中使用sbt需要提供两个文件:
- project/build.properties
- build.sbt
第一个文件用于指定sbt所使用的版本,第二个文件定义了整个构建的配置信息:
// project/build.properties
sbt.version=0.13.7
// build.sbt
name := "preowned-kittens" // 构建名称
// 使用空行隔开
version := "1.0" // 构建版本
在sbt中提供了很多命令,其中有一个tasks. tasks是sbt能够为你完成的工作,比如编译一个项目,创建文档或者运行测试.在sbt中输入tasks命令,会输出它提供的所有命令.
输入settings,会显示所有可以获取当前项目构建属性的命令.
Creating builds
如之前的介绍,我们可以按如下方式创建一个build.sbt
文件:
name := "preowned-kittens"
organization := "com.preowned-kittens"
version := "1.0"
libraryDependencies += "org.specs2" % "specs2_2.10" % "1.14" % "test"
配置中每一项都有三个部分组成,Key,Operator,Initialization
.
创建一个task:
val gitHeadCommitSha = taskKey[String]( "Determines the current git commit SHA")
gitHeadCommitSha
是key的名字taskKey
表示这个key只用于task[String]
表示这个task的返回值类型为String
- 最后的字符串部分用于在解释器中显示该key的描述
请求这些值的时候,这些task会被运行.
比如创建一个task,用于在构建时读取git的commitHash值,并生成一个version.properties
文件以便网站能够在运行时读取,这样就可以总是能够知道当前代码使用的版本了.
gitHeadCommitSha := Process("git rev-parse HEAD").lines.head
然后就可以在sbt中使用该变量:
> show gitHeadCommitSha
这些是sbt中的定义部分,可以定义一个变量或方法在sbt配置中进行重用,这些定义会首先被编译,并且可以引用之前的定义.当请求一个task时,会对所有它依赖的task执行计算.
下面的例子中生成一个version.properties
文件:
val makeVersionProperties = taskKey[Seq[File]]("Makes a version.properties file.")
makeVersionProperties := {
val propFile = new File((resourceManaged in Compile).value, "version.properties")
val content = "version=%s" format (gitHeadCommit.value)
IO.write(propFile, content)
Seq(propFile)
}
- 首先创建一个taskKey来初始化一个task
- 然后定义了这个task如何执行
然后需要告诉sbt在编译时将这个属性文件包含在运行时的classpath中:
resourceGenerators in Compile += makePropertiesFile
使用配置
配置是key的命名空间,这允许一个key用于多个不同的目的.sbt在默认的构建中带有多种不同的配置:
Configuration | Purpose |
---|---|
Compile | 这些设置和值用于编译主项目并生成生产环境的artifact |
Test | 用于编译和运行单元测试代码 |
Runtime | 用于在sbt中运行工程 |
IntegrationTest | 根据生成环境artifact运行测试 |
这些配置用于根据高阶规则对setting和task进行拆分.比如sources in Compile
会为编译生产环境artifact收集资源文件,sources in Test
则用于定义单元测试需要的资源文件.
可以把sbt文件中的各个setting和task看做是一个excel文件中的各个列,列的每一行定义了他们的值,而下面的行会对上面的行进行修改会添加.而不同的配置则可以看做是excel文件中的多个worksheet,不同的worksheet用于不同的使用目的.
定义多个子工程
比如需要在一个工程中包含两个项目,一个web网站,一个数据分析,这两个应用都会用到一些公共的代码.这时需要定义一个子工程为common
,用于存放一些通用的模块及测试.
创建一个新的sbt工程,然后在build.sbt
文件中添加如下代码:
lazy val common = ( Project("common", file("common")). settings() )
common
定义了一个工程Project("common"
部分定义了在sbt控制台中的工程名file("common")
第一了用于检索该工程的磁盘位置settings()
部分为该工程添加额外的配置,只是现在为空
进入sbt控制台,编译后输入projects
命令就可以看到所有的工程列表,包含刚刚创建的common
工程.
使用common/test
命令可以执行common工程下的测试.如果在子工程需要使用以来库,则需要在其setting中单独配置:
lazy val common = (
Project("common", file("common"))
settings( libraryDependencies += "org.specs2" % "specs2_2.10" % "1.14" % "test" )
)
这时添加另外两个子项目:
lazy val analytics = (
Project("analytics", file("analytics"))
dependsOn(common) settings()
)
lazy val website = (
Project("website", file("website"))
dependsOn(common) settings()
)
上面使用了Project
的dependsOn
方法定义了这两个项目均以来与common
项目.在使用这两个项目时首先会对他们所以来的项目进行编译.
Putting it all together
上面的项目中,所有的子工程都使用工程名来作为文件夹名.这个方便的特性帮助开发者找到正确的工程并知道如果用来构建.相对于来回复制文件夹或名字字符串,可以创建一个辅助方法来结构化工程配置.下面在build.sbt
的首部定义一个方法:
def PreownedKittenProject(name: String): Project = (
Project(name, file(name))
)
这个被命名为PreownedKittenProject
的方法接收一个工程名作为参数,返回一个sbt.Project
对象,位置名和工程名一样.然后使用这个方法重新配置工程:
lazy val common = (
PreownedKittenProject("common"). settings(
libraryDependencies += "org.specs2" % "specs2_2.10" % "1.14" % "test" )
)
val analytics = (
PreownedKittenProject("analytics"). dependsOn(common). settings()
)
val website = (
PreownedKittenProject("website"). dependsOn(common). settings()
)
可以注意到dependsOn
和settings
调用跟之前没有什么变化,这是因为PreownedKittenProject
返回的是一个base-level
的工程,可以用来细化配置依赖和setting.这表示所有在PreownedKittensProject
中定义的属性或配置会应用到所有使用该方法构建的子项目.
可以继续更新这个PreownedKittenProject
方法,以便配置基本的组织信息,版本,并且测试依赖库也能包含到所有子工程中:
def PreownedKittenProject(name: String): Project = (
Project(name, file(name)).
settings(
version := "1.0",
organization := "com.preownedkittens",
libraryDependencies += "org.specs2" % "specs2_2.10" % "1.14" % "test"
)
)
lazy val common = (
PreownedKittenProject("common").
settings()
)
val analytics = (
PreownedKittenProject("analytics").
dependsOn(common).
settings()
)
val website = (
PreownedKittenProject("website").
dependsOn(common).
settings()
)
val gitHeadCommitSha = taskKey[String]("Determines the current git commit SHA")
gitHeadCommitSha := Process("git rev-parse HEAD").lines.head
val makeVersionProperties = taskKey[Seq[File]]("Creates a version.properties file we can find at runtime.")
makeVersionProperties := {
val propFile = (resourceManaged in Compile).value / "version.properties"
val content = "version=%s" format (gitHeadCommitSha.value)
IO.write(propFile, content)
Seq(propFile)
}
对于version.properties
文件,只能在common中拥有一个以便其他连个子项目都能使用.以便区分在运行时是否使用了一致的版本.
可能gitHeadCommitSha
这个task可能会用于其他用途,而只有生成version.properties
是我们在common
中需要的,所以只把文件生成部分放到common
中,而gitHeadCommitSha
放在build自身.
使用in ThisBuild
把task定义带build自身:
gitHeadCommitSha in ThisBuild := Process("git rev-parse HEAD").lines.head
这样这个task就被附到build自身了,并且所有的自工程都可以使用它的结果.然后把makeVersionProperties
移动到common
工程:
lazy val common = (
PreownedKittenProject("common").
settings(
makeVersionProperties := {
val propFile = (resourceManaged in Compile).value /"version.properties"
val content = "version=%s" format (gitHeadCommitSha.value)
IO.write(propFile, content)
Seq(propFile)
}
)
)
编译代码
使用inspect tree
命令可以查看sbt的需要,比如输入inspect tree compile:compile
命令,会输出一个ASCII
树,包含编译所需要的tasks和settings以及它们返回的值.
Finding your source
源文件
几乎所有的工程都会用到一些源文件,sbt提供了源文件的管理,通知支持自定义.
输入inspect tree source
可以获得源文件的结构树.
unmanagedSources
: 一个已发现的源文件列表,使用标准的工程惯例managedSources
: 一个由build生产的或者手动添加的源文件列表
对于sbt来说,unmanagedSources
由convention
发现.类似于内存管理.Unmanaged
的意思是你(而不是sbt)可以添加,修改或跟踪这些源文件,同时managed
源文件而是由sbt为你创建和跟踪的.
unmanagedSources
使用一组文件过滤器和一组默认的文件夹来为工程生成源文件列表,文件夹是用javaSource
和scalaSource
来定义,sbt用他们来查找源文件,使用命令可以查看他们的默认设置:
> show javaSource
[info] <project-dir>/src/main/java
> show scalaSource
[info] <project-dir>/src/main/scala
同时还有一些生产环境资源文件,同时被引用为compile sources
,这些是.scala
或.java
文件,被编译成成二进制文件然后放到生产环境.
资源文件
另外,很多工程拥有compile sources
,是一些用于运行时而又不需要编译的.比如.properties
和.xml
文件.这些资源文件不会被编译,只是被拷贝到二进制的artifact中.
类似于sbt的sources
的task用于收集源,同时还有一个resources
用于收集运行时需要的文件.
managedResources
: 一组由手动指定或为build手动生成的unmanagedResources
: 一组在资源文件夹中找到的文件
与源文件管理不同的是,资源文件并不使用过滤器,所有在资源文件路径找到的文件都会在运行时可用.
查看资源文件路径:
> show resourceDirectory
[info] <project-dir>/src/main/resources
测试源文件
除了上面的source和resource,还有一种test source files
包含了测试代码并且永远不会进入生产环境.或者可以作为test resources
,一种不会进行编译,只会在运行时执行的文件.
查看测试源的依赖树:
> inspect tree test:sources
[info] test:sources = Task[scala.collection.Seq[java.io.File]]
...
自定义源文件组织
sourceDirectory
默认被指定为src
目录,sbt默认的配置为:
sourceDirectory := new File(baseDirectory.value, "src")
如果需要自定义为sources/
:
sourceDirectory := new File(baseDirectory.value, "sources")
然后是main和test的路径,他们依赖于sourceDirectory
配置,默认的配置为:
sourceDirectory in Compile := new File(sourceDirectory.value, "main")
sourceDirectory in Test := new File(sourceDirectory.value, "test")
如果需要把把main改为production/
路径:
sourceDirectory in Compile := new File(sourceDirectory.value, "production")
过滤需要的源文件
includeFilter in (Compile, unmanagedSources) := "*.scala"
excludeFilter in (Compile, unmanagedSources) := NothingFilter
依赖库
使用inspect tree compile:dependencyClasspath
查看依赖库的依赖树.
- Internal dependencies: 定义在当前sbt的build中,用于各个工厂的依赖.
- External dependencies: 从其他外部拉去的依赖,通过Ivy或文件系统.
内部依赖通过dependsOn
方法计算得出,同时分为两个部分:
- Unmanaged dependencies: sbt从默认的位置发现,比如工厂的lib文件夹
- Managed dependencies: 在sbt的build中指定的,通过
libraryDependencies
添加的
工程打包
工程标识:
mappings in packageBin in Compile += (baseDirectory.value / "LICENSE") -> "PREOWNED-KITTEN-LICENSE"
name := "preownedkittens-core"
organization := "org.lostkittens"
version := "1.0.0"
Effective sbt
总是指定sbt的版本:
// poject/build.properties sbt.version=0.13.11
在一个地方追踪依赖:
// project/Dependencies.scala object Dependencies { // Versions val akkaVersion = 2.4.2 // Libraries val specs2 = "org.specs2" %% "specs2" % "1.14" val akkaActor = "com.typesafa.akka" %% "akka-actor" % akkaVersion // Projects val akkaProjectDependencies = Seq(akkaActor, specs2 % "test") } // Usage import Dependencies._ lazy val akkaBackend = ( Project("akkaBackend", file(".")). settings( libraryDependencies += akkaProjectDependencies ) )
为plugin文件命名:
// project/play.sbt <= plugins for play val playVersion = "2.5.1" resolvers += "Typesafe repository" at "https://dl.bintray.com/typesafe/maven-releases/" addSbtPlugin("com.typesafe.play" % "sbt-plugin" % playVersion)
在多个工程中共享配置:
// build.sbt val commonSettings = Seq( organization := "com.typesafe.example" ) val web = ( Project("backend", file(".")). settings(commonSettings:_*) // 对commonSettings进行重用 )
或者将辅助方法放到一个类似
lib
的文件project/*.scala
中.或者创建一个新的工程构造器.// project/common.scala import sbt._ import Keys._ object common { val commonSettings = Seq( organization := "com.typesafe.example" ) def AwesomeProject(name:String) = ( Project(name, file(name)). settings.(commonSettings:_*) ) def AwesomePlayProject(name:String) = ( play.Project(name, path=file(name)). settings(commonSettings:_*) ) } // Usage // build.sbt lazy val backend = ( AwesomeProject("backend"). settings(backendDependencies:_*) )
从对个工程中聚合tasks:
lazy val root = (project in file(".")). aggregate(util, core) lazy val util = project lazy val core = project
为版本进行版本控制:
val gitHeadCommitSha = settingKey[String]("current git commit SHA") gitHeadCommitSha in ThisBuild := Process("git rev-parse HEAD").lines.head version in ThisBulid := "1.0-" + gitHeadCommitSha.value
或者发布的版本,如果作为发布版本则设置为
1.0
,否则追加上对应的版本号:val release = settingKey[Boolean]("") release := sys.props("release") == "true" version in ThisBuild := { val v = "1.0" if(release.vaule) v else s"$v-${gitHeadCommitSha.value}" }
使用sbt的IO操作,
Process
和File
库:前面用到的一个例子:
val cmd = "git rev-parse HEAD" Process(cmd).lines.head
我们可以用这样的方法在代码中获取工程的名字或版本:
// project/buildInfo.scala val infoFiles = taskKey[Seq[File]]("") infoFiles := { val base = (sourceManaged in Compile).value val file = base / "buildInfo.scala" val content = s"""object BuildInfo { version = "${version.value}" }""" IO.write(file, content) Seq(file) } sourceGenerators in Compile <+= infoFiles // Usage val commonSettings = BuildInfo.settings ++ ...
使用settings和方法来保持代码清晰并易于扩展:
// project/buildInfo.scala object BuildInfo { val properties = settingKey[Map[String, String]] val infoFile = taskKey[File] .... def makeInfo(file:File, props:Map[String,String]) = { val lines = for((key,value) <- props) yield s"val $key=\"$value\"" val source = s"""object BuildInfo{ ${lines.mkString "\n"} }""" IO.write(source, file) file } val settings = Seq( properties := Map.empty properties += "version" -> version.value properties += "name" -> name.value infoFile := { makeInfo( (sourceManaged in Compile).vaule / "buildInfo.scala", properties.value ) } ), sourceGenterators in Compile += infoFile.task }
使用插件:
addSbtPlugin("com.eed3i9n" % "sbt-buildinfo" % "0.2.5") // Usage val commonSettings = buildInfoSettings ++ ...
使用
~
符号响应实时改变.