Simple SBT: Getting start

简介

Sbt由两个部分组成,task和setting.

  1. tasks: Sbt围绕task进行构建.跟Maven不同的是,没有阶段,目标,执行,而只是task.想做一些事情的生活只需要执行task.如果想要一个task在一个task之后执行,只需要在两个task之间添加隐式依赖.如果需要在一个task中使用另一个task的结果,可以将task的输出推送到另一个task,task的结果会在依赖他的task中自动可见.Sbt中task的输出是一个值,可以是任何类型,因此很易于传递.多个task可以依赖于同一个task的结果.默认情况下sbt会以并行的方式运行这些task,同时会使用依赖树,以此来决定哪些顺序执行,哪些并行执行.
  2. settings: 一个setting在sbt中是一个值,可以是项目的名字或者scala的版本.

Sbt所提供的特性:

  1. Reproducible builds: 并不关心谁来构建.
  2. Minimal configuration: 更快的配置并运行.
  3. Encouragement of good practices: 比如在打包之间运行测试.
  4. Portability: 可以在任何机器上运行.
  5. A good ecosystem: 开发者需要做的很少.

同时提供了交互式的环境,会提高开发者的生产力提供了一下特性:

  1. Faster compilation times: 更少的启动时间,增量编译
  2. Easy addition of custom tasks: 简便的构建定义
  3. Faster compile/test cycles through the use of the ~ command: 快速反馈
  4. 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")
  1. gitHeadCommitSha是key的名字
  2. taskKey表示这个key只用于task
  3. [String]表示这个task的返回值类型为String
  4. 最后的字符串部分用于在解释器中显示该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)
}
  1. 首先创建一个taskKey来初始化一个task
  2. 然后定义了这个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用于不同的使用目的.

sbt-configuration

定义多个子工程

比如需要在一个工程中包含两个项目,一个web网站,一个数据分析,这两个应用都会用到一些公共的代码.这时需要定义一个子工程为common,用于存放一些通用的模块及测试.

创建一个新的sbt工程,然后在build.sbt文件中添加如下代码:

lazy val common = ( Project("common", file("common")). settings() )
  1. common定义了一个工程
  2. Project("common"部分定义了在sbt控制台中的工程名
  3. file("common")第一了用于检索该工程的磁盘位置
  4. 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() 
)

上面使用了ProjectdependsOn方法定义了这两个项目均以来与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() 
)

可以注意到dependsOnsettings调用跟之前没有什么变化,这是因为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可以获得源文件的结构树.

  1. unmanagedSources: 一个已发现的源文件列表,使用标准的工程惯例
  2. managedSources: 一个由build生产的或者手动添加的源文件列表

对于sbt来说,unmanagedSourcesconvention发现.类似于内存管理.Unmanaged的意思是你(而不是sbt)可以添加,修改或跟踪这些源文件,同时managed源文件而是由sbt为你创建和跟踪的.

unmanagedSources使用一组文件过滤器和一组默认的文件夹来为工程生成源文件列表,文件夹是用javaSourcescalaSource来定义,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用于收集运行时需要的文件.

  1. managedResources: 一组由手动指定或为build手动生成的
  2. 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查看依赖库的依赖树.

  1. Internal dependencies: 定义在当前sbt的build中,用于各个工厂的依赖.
  2. External dependencies: 从其他外部拉去的依赖,通过Ivy或文件系统.

内部依赖通过dependsOn方法计算得出,同时分为两个部分:

  1. Unmanaged dependencies: sbt从默认的位置发现,比如工厂的lib文件夹
  2. 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

  1. 总是指定sbt的版本:

    // poject/build.properties
    sbt.version=0.13.11
    
  2. 在一个地方追踪依赖:

    // 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
        )
    )
    
  3. 为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)
    
  4. 在多个工程中共享配置:

    // 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:_*)
    )
    
  5. 从对个工程中聚合tasks:

    lazy val root = (project in file(".")).
      aggregate(util, core)
    
    lazy val util = project
    
    lazy val core = project
    
  6. 为版本进行版本控制:

    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}"
    }
    
  7. 使用sbt的IO操作,ProcessFile库:

    前面用到的一个例子:

    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 ++ ...
    
  8. 使用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
    }
    
  9. 使用插件:

    addSbtPlugin("com.eed3i9n" % "sbt-buildinfo" % "0.2.5")
    
    // Usage
    val commonSettings = buildInfoSettings ++ ...
    
  10. 使用~符号响应实时改变.