build.sbt的定义格式

一个简单的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目录即相关文件介绍

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目录及其相关文件。

Managed Dependencies详解

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"

.scala形式的build定义

对于简单的项目来说,.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项目结构的本质

在了解了.sbt和.scala两种形式的build定义形式以后, 咱们就能够来看看SBT项目构建结构的本质了。

首先, 一个SBT项目,与构建相关联的基本设施能够概况为3个部分, 即:

  1. 项目的根目录, 好比hello/, 用来界定项目构建的边界;
  2. 项目根目录下的*.sbt文件, 好比hello/build.sbt, 用来指定通常性的build定义;
  3. 项目根目录下的project/*.scala文件, 好比hello/project/Build.scala, 用来指定一些复杂的, *.sbt形式的build定义文件不太好搞的设置;

也就是说, 对于一个SBT项目来讲,SBT在构建的时候,只关心两点:

  1. build文件的类型(是*.sbt仍是*.scala);
  2. build文件的存放位置(*.sbt文件只有存放在项目的根目录下, SBT才会关注它或者它们, 而*.scala文件只有存放在项目根目录下的project目录下,SBT才不会无视它或者它们)^[实际上,只有那些定义了扩展自sbt.Build类的scala文件,才会被认为是build定义];

在以上基础规则的约束下,咱们来引入一个推导条件, 即:

项目的根目录下的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其实很简单,就跟把大象关冰箱里同样简单, 概况来讲其实就是:

  1. 定义task;
  2. 将task添加到项目的settings当中;
  3. 使用自定义的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添加到项目的settings当中

光完成了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

万事俱备以后,就可使用咱们的自定义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(f, binDir / f.name)) s.log.info("[dist] copy configuration templates to conf...") ((baseDir / "conf") * "*.xml").get.foreach(f => IO.copyFile(f, confDir / f.name)) s.log.info("[dist] copy examples chanenl deployment...") IO.copyDirectory(baseDir / "examples", assemblyDir / "examples") res.filter(_.name.startsWith("logback")).foreach(f => IO.copyFile(f, assemblyDir / f.name)) } }

这种方式好是好,可就是不够通用,你我应该都不想每一个项目里的Build文件里都拷贝粘帖一把这些代码吧?! 何况, 哪些artifacts要打包进去,打包以前哪些参数能够调整,以这种形式来看,都不方便调整(若是你不烦每次都添加修改代码的话), 那SBT有没有更好的方式来支持相似的需求那?! 固然有咯, SBT的Plugins机制就是为此而生的!

SBT Plugin机制容许咱们扩展SBT项目的build定义, 这里的扩展基本能够理解为容许咱们向项目的build定义里添加各类所需的Settings, 好比自定义Task,瞅一眼Plugin的代码就更明了了:

trait Plugin { def settings: Seq[Setting[_]] = Nil }

咱们知道若是项目位于hello目录下的话, 该项目的build定义能够位于 hello/\*.sbt 或者 hello/project/*.scala 两种位置,既然Plugin是对build定义的扩展,那么, 咱们就能够认为项目的build定义依赖这些plugin的某些状态或者行为,即plugin属于项目build定义的某种依赖,从这个层次来看,plugin的配置和使用跟library dependency的配置和使用是同样的(具体有稍微的差别)。不过,既然plugin是对build定义的扩展(及被依赖),那么,咱们应该在build定义对应的SBT项目的build定义中配置它(听起来是否是有些绕? 若是读者感受绕,看不明白的话,不妨回头看看"SBT项目结构的本质"一节的内容),即 hello/project/\*.sbt 或者 hello/project/project/\*.scala , 大多数状况下,咱们会直接在像 hello/project/plugins.sbt ^[plugins.sbt的名称只是便于识别,实际上,SBT只关注.sbt的后缀,具体名称是不关心的,由于plugins.sbt本质上也是一个SBT项目的build定义文件,除了在其中配置Plugin,咱们一样能够添加第三方依赖, 追加其余Setting等,跟通常的.sbt配置文件无异]配置文件中配置和添加Plugin:

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")

由于Plugin实现通常也都是以三方包的形式发布的, addSbtPlugin 所作的事情实际上就是根据Plugin发布时使用的artifactId等标志性信息^[在SBT中,使用ModuleID来抽象和标志相应三方库的标志],将它们转换成Setting添加到当前项目build定义中。

若是Plugin是发布到SBT默认会查找的Maven或者Ivy Repository中,则只须要 addSbtPlugin 就好了, 不然, 须要将Plugin发布到的Repository添加到resolvers以便SBT能够发现并加载成功。

有哪些现成的Plugin能够用吗?

在前面的配置实例中,咱们已经看到两个笔者经常使用的Plugin:

  1. sbt-idea
    • 笔者使用IntelliJ IDEA来开发scala应用, sbt-idea这个插件能够帮助我从SBT的配置中生成IDEA这个IDE对应的classpath,项目信息等元数据, 这样,我只要运行 sbt gen-idea 这一命令以后,就能够在IDEA中直接打开当前的SBT项目了。若是读者使用其余的IDEA,那也可使用相似的插件,好比 sbteclipse 或者 sbt-netbeans-plugin
  2. sbt-dependency-graph
    • 项目的依赖越多, 依赖关系就越复杂, 这个插件能够帮助咱们理清楚项目各个依赖之间的关系,完成跟Maven的dependency:tree相似的功能

除了这些, 读者还能够在SBT的 Plugins List 中找到更多有用的Plugin,好比:

    1. xsbt-web-plugin
      • 看名字就能猜到是干啥的了
    2. sbt-assembly
      • 能够将当前项目的二进制包以及依赖的全部第三方库都打包成一个jar包发布,即one-jar, 对于那种直接运行的应用程序很方便
    3. sbt-aether-deploy
      • 使用aether来部署当前项目, aethor是Maven中管理Repository的API,如今单独剥离出来更通用了
    4. sbt-dirty-money

      • SBT会将项目的依赖抓取到本地的ivy cache中缓存起来,避免频繁的update对带宽形成的浪费,但有些时候须要对缓存里失效的内容进行清理,使用这个插件能够避免本身手动遍历目录逐一删除相应的artifacts
        关于现成可用的Plugin就介绍这些,更多的好东西仍是有待读者本身去发掘吧!

      TIPS

      若是某些SBT Plugin我的常常用到,那么,能够将这些Plugin配置为Global plugin, 即在用户的home目录下的".sbt/plugins/"目录下新建一个plugins.sbt文件(名称无所谓,类型有所谓,你懂的哦),而后将这些经常使用的插件配置进去,以后,在任何的SBT项目下,就均可以使用这些插件了,“配置一次,处处运行”!

      不过, 笔者建议, 跟项目相关的SBT Plugin仍是应该配置到当前项目中,这样,走版本控制,别人均可统一使用这些Plugin,只有哪些本身经常使用,而与具体项目绑定关系不是很强的Plugin才配置为global的plugin, 道理跟git和svn处理ignore的作法差别是相似的!

想写个本身的SBT Plugin该咋整?!

编写一个本身的SBT Plugin其实并不复杂,一个SBT Plugin工程跟通常的SBT项目并没有质上的差别,惟一的差异就在于咱们须要在SBT Plugin项目的build定义中指定一项Setting用来代表当前项目是一个SBT Plugin项目,而不是一个通常的SBT项目,这项Setting即:

sbtPlugin := true

有了这项Setting, SBT在编译和发布当前这个Plugin项目的时候就会作两个事情:

  1. 将SBT API加入当前项目的classpath中(这样咱们就能够在编写Plugin的时候使用到SBT的API);
  2. 在发布(publish-local, publish)当前项目的时候,SBT会搜寻sbt.Plugin的实现,而后将这些实现添加到sbt/sbt.plugins这个文件中,并将这个文件与当前Plugin项目的其它artifacts一块儿打包到jar中发布;

Plugin项目发布以后,就能够在其余项目中引用它们,怎么用,前面详细介绍过了,这里再也不赘述。

有了编写SBT Plugin理论指导,咱们就能够着手实践了, 咱们先把hello这个自定义task转换为Plugin实现以下:

// HelloSBT.scala

import sbt._ object HelloSBT extends Plugin { val helloSbt = TaskKey[Unit]("hello-sbt", "just say hello") val helloSbtSetting = helloSbt := { println("hello, sbt~") } }

而后,咱们为其配置build定义:

// ${project.root}/build.sbt

name := "hello_sbt_plugin" organization := "com.github.fujohnwang" version := "0.0.1" sbtPlugin := true scalaVersion := "2.9.2"

编译并发布到本地的ivy库中(测试无误后,能够直接发布到其余共享范围更大的repo中),执行:

sbt publish-local

以后,咱们就能够在其余SBT项目的build定义中使用到这个SBT Plugin了,好比咱们将其添加到某个SBT项目的${project.root}/project/plugins.sbt(名称不重要,还记得吧? 注意路径):

addSbtPlugin("com.github.fujohnwang" % "hello_sbt_plugin" % "0.0.1")

而且将__helloSbtSetting__手动配置到${project.root}/build.sbt中:

HelloSBT.helloSbtSetting

好啦,如今算是万事大吉了,在当前SBT项目直接运行 sbt hello-sbt 试试吧!

理论和实践咱们都阐明了,如今该说一下在编写SBT Plugin的时候应该注意的一些事情了。

首先,回头查看HelloSBT的代码实现,读者应该发现,咱们将SettingKey的名称和变量名都作了改变,这是由于咱们在编写Plugin的时候,要尽可能避免命名冲突,因此,经过引入当前Plugin的名称前缀来达到这一目的;

其次,咱们没有将helloSbtSetting加入到 Plugin.settings (这里一样注意添加了前缀的命名方式),这就意味着,咱们在使用这一Plugin的时候,要手动将这一Setting加到目标项目的build定义中才能使用到它,缘由在于,虽然咱们能够override并将helloSbtSetting加入 Plugin.settings ,这样可让SBT自动加载到当前项目,但通常状况下咱们不会这样作,由于在多模块的项目中,这一setting也会自动加载到全部项目上,除非是command类型的Plugin,不然,这种行为是不合适的, 故此,大部分Plugin实现都是提供本身的Setting,并让用户决定是否加载使用;

其实,编写一个SBT Plugin还要注意不少东西,但这里就不一一列举了,你们能够参考 Best PracticesPlugins Best Practices 这两份SBT Wiki文档,里面详细说明了编写SBT Plugin的一些最佳实践,不过,做为结束,我补充最基本的一点, 即"不要硬编码Plugin实现的任何配置"! 读者能够尝试将dist自定义task转换成一个SBT Plugin,在转换过程当中,不妨为"dist"啦, "conf"啦, "bin"啦这些目标目录设立相应的SettingKey并给予默认值,这样就不会像咱们的自定义task里似的,直接硬编码这些目录名称了,并且,插件的使用者也能够在使用插件的项目中经过override相应的Plugin的这些SettingKey标志的Setting来提供自定义的值, 怎么样? 动手尝试一把?!

NOTE

要编写一个SBT Plugin还须要修炼一下SBT的内功,包括搞清楚SBT的Setting系统,Configuration,Command等深层次概念, 这样,在编写SBT Plugin的时候才不会感受“局促”,^_^

多模块工程管理(Multi-Module Project)

对于Maven用户来讲, 多模块的工程管理早就不是什么神秘的特性了吧?! 但笔者实际上对于这种工程实践却一直持保留意见,由于不少时候,架构项目结构的人并无很好的理解项目中各类实体的粒度与边界之间的合理关系, 不少明明在package层次/粒度能够搞定的事情也每每被归入到了子工程的粒度中去,这种不合适的粒度和边界选择,一方面反映了最初规划项目结构的人对自身项目的理解不足,另外一方面也会为后继的开发和维护人员带来些许的繁琐。因此不少时候,若是某些关注点足以设立一个项目来管理,那我宁愿直接为其设立独立的项目结构,而后让须要依赖的项目依赖它便可以了(大部分时候,咱们要解决的就是各个项目之间的依赖关系,不是吗?),固然, 这种作法并不是绝对,只是更多的在强调粒度和边界选择的合理性上。

扯多了,如今让咱们来看看在SBT中咱们是如何来规划和组织多模块的项目结构的吧!

包含多个子模块或者子项目的SBT项目跟一个标准的独立的SBT项目相差很少,惟一的差异在于:

  1. build定义中多了对多个子模块/工程的关系的描述;
  2. 项目的根目录下多了多个子模块/工程相应的目录;

下面是一个多模块工程的典型结构:

${project.root}
    - build.sbt
    + src/main/scala
    + project
        - Build.scala
    + module1
        - build.sbt
        + src/main/scala
    + module2
        - build.sbt
        + src/main/scala
    + module3
        - build.sbt
        + src/main/scala

咱们能够发现,除了多了各个子模块/工程相应的目录,其它方面跟一个标准独立的SBT项目无异, 这些子模块/工程与当前项目或者其它子模块/工程之间的关系由当前项目的build定义来“说明”, 固然这种关系的描述是如此的“纠缠”,只能在*.scala形式的build定义中声明, 例如在${project.root}/project/Build.scala)中咱们能够简单的定义多个子模块/工程之间的关系以下:

import sbt._ import Keys._ object MultipleModuleProjectBuild extends Build { lazy val root = Project(id = "root", base = file(".")) aggregate(sub1, sub2) lazy val sub1 = Project(id = "m1", base = file("module1")) lazy val sub2 = Project(id = "m2", base = file("module2")) dependsOn(sub3) lazy val sub3 = Project(id = "m3", base = file("module3")) }

在当前项目的build定义中,咱们声明了多个Project实例来对应相应的子项目,并经过Porject的aggregate和dependsOn来进一步申明各个项目之间的关系。 aggregate指明的是一种并行的相互独立的行为,只是这种行为是随父项目执行相应动做而触发的, 好比,在父项目中执行compile,则会同时触发module1和module2两个子模块的编译,只不过,两个子模块之间的执行顺序等行为并没有联系; 而dependsOn则说明一种强依赖关系, 像module2这个子项目,由于它依赖module3,因此,编译module2子模块/项目的话,会首先编译module3,而后才是module2,固然,在module3的相应artifact也会加入到module2的classpath中。

咱们既然已经了解了如何在父项目中定义各个子模块/项目之间的关系,下面咱们来看一下各个子模块/项目内部的细节吧! 简单来说, 每一个子模块/项目也能够看做一个标准的SBT项目,但一个很明显的差别在于:  每一个子模块/项目下不能够再建立project目录下其下相应的*.scala定义文件(实际上能够建立,但会被SBT忽略) 。不过, 子模块/项目本身下面仍是可使用.sbt形式的build定义的,在各自的.sbt build定义文件中能够指定各个子模块/项目各自的Setting或者依赖,好比version和LibrarayDependencies等。

通常状况下, SBT下的多模块/工程的组织结构就是如此,即由父项目来规划组织结构和关系,而由各个子模块/项目自治的管理各自的设置和依赖。但这也仅仅是倡导,若是读者愿意,彻底能够在父项目的Build定义中添加任何本身想添加的东西,好比将各个子模块/项目的Settings直接挪到父项目的.scala形式的build定义中去(只不过,可能会让这个.scala形式的build定义看起来有些臃肿或者复杂罢了), 怎么作? 本身查sbt.Project的scaladoc文档去 :)

总的来讲,多子模块/工程的项目组织主体上仍是以父项目为主体,各个子模块/项目虽然有必定的“经济”独立性,但并不是彻底自治, 貌似跟未成年人在各自家庭里的地位是类似吧?! 哈~

TIPS

在Maven的多子模块/项目的组织结构中,咱们很喜欢将全部项目能够重用或者共享的一些设定或者依赖放到父项目的build定义中,在SBT中也是能够的,只要在父项目的.sbt或者.scala形式的build定义中添加便可,不过,要将这些共享的设定的scope设定为ThisBuild, 例如:

    scalaVersion in ThisBuild := "2.9.2"

这样,就不用在每个项目/模块的build定义中逐一声明要使用的scala版本了。其它的共享设定能够依法炮制哦~
相关文章
相关标签/搜索