经过几个小时的吐血,踩了无数的坑以后,终于成功的创建一个简单的sbt插件。其功能异常简单:使用该插件后,可以执行任务hello
,输出一段文字。
一个sbt插件项目,首先是一个普通的sbt项目,只是有一些特殊的配置,以及需要继承一些特殊的类。
可以使用下面的命令创建必要的目录和文件:
mkdir my-sbt-plugin
cd my-sbt-plugin
touch build.sbt
mkdir project
touch project/build.properties
mkdir -p src/main/scala
touch src/main/scala/MyPlugin.scala
然后向build.sbt
中填入以下内容:
name := "my-sbt-plugin"
version := "0.1.0"
organization := "test20140913"
这三个配置非常重要,因为插件发布以后,其它项目使用的时候,需要这样导入它:
addSbtPlugin("test20140913" %% "myplugin" % "0.1.0")
这里我使用了一个奇怪的organization名字test20140913
,是为了方便本地测试。因为在测试时,需要把这个包发布到本地,如果起一个正常的如com.xxx
的名字,到时候不好找。可以在调好以后再改名。
在sbt 0.13.5中,sbt提供了一种新的插件机制AutoPlugin
,可以让插件自动被sbt发现并载入,不再像以前那样必须在build.sbt
里添加一些初始化代码。
我们的插件将使用这种方式,所以我们需要指定sbt版本。
可以在project/build.properties
中填入以下内容:
sbt.version=0.13.5
或者,直接在build.sbt
里添加:
sbtVersion := 0.13.5
向build.sbt
中添加:
sbtPlugin := true
它将自动将sbt作为依赖添加到我们项目中,方便我们在代码中引用sbt中定义的类。
我们需要创建一个继承自AutoPlugin
的object
,名字可以随便取,这里我使用MySbtPlugin
。它是插件的入口。
这个MySbtPlugin.scala
应该放在哪里呢?
src/main/scala/
下这两个位置都是sbt可以自动发现、编译和处理的。
不能放在project
下,因为这里的scala文件都是为了配置sbt的构建命令而存在的,如果放在这里,它们不会被发布出去。
需要放在一个package下面吗?还是可以直接弄成顶层object?
这里是一个坑,因为在sbt 0.13.5
中,必须放在某个包下面,否则会报下面的错:
[info] Compiling 2 Scala sources to /sbttest/my-sbt-plugin/project/target/scala-2.10/sbt-0.13/classes...
/sbttest/my-sbt-plugin/build.sbt:0: error: '.' expected but eof found.
import _root_.sbt.plugins.IvyPlugin, _root_.sbt.plugins.JvmPlugin, _root_.sbt.plugins.CorePlugin, _root_.sbt.plugins.JUnitXmlReportPlugin, MyPlugin
而在sbt 0.13.6
中已经修复了。
为了能在当前的主流版本sbt 0.13.5
中正常运行,我决定还是把它放在某个包下。
随便什么包名都行,也不需要为这个包名专门创建一个目录。我打算使用当前的日期test20140913
作为包名,以后可以改个正常点的。
最后,我在src/main/scala/MySbtPlugin.scala
中,写入以下代码:
package test20140913
import sbt._
object MySbtPlugin extends AutoPlugin {
}
到现在为止,一个什么也不能做的sbt plugin就完成了。前面提到的hello
的功能呢?我们晚点在后面实现。
我们可以先把它发布到本地试试,看看效果。
需要使用sbt
的publicLocal
命令,把它发布到本地。
因为我们用到的AutoPlugin
功能比较新,所以先升级到最新的版本比较保险。
首先看一下你本地安装的sbt版本是多少:
$ sbt --version
sbt launcher version 0.13.5
如果低于0.13.5
,需要升级一下。比如在mac下,可以使用brew
:
brew update
brew upgrade sbt
当前它支持的最新版本是0.13.5
。
sbt 0.13.6
也发布了,应该很快能更新。
$ cd my-sbt-plugin
$ sbt
> publishLocal
输出:
> publishLocal
[info] Packaging /sbttest/myplugin/target/scala-2.10/sbt-0.13/my-sbt-plugin-0.1.0-sources.jar ...
[info] Done packaging.
[info] Updating {file:/sbttest/myplugin/}myplugin...
[info] Wrote /sbttest/myplugin/target/scala-2.10/sbt-0.13/my-sbt-plugin-0.1.0.pom
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] :: delivering :: test20140913#my-sbt-plugin;0.1.0 :: 0.1.0 :: release :: Sun Sep 14 12:09:04 CST 2014
[info] delivering ivy file to /sbttest/myplugin/target/scala-2.10/sbt-0.13/ivy-0.1.0.xml
[info] Main Scala API documentation to /sbttest/myplugin/target/scala-2.10/sbt-0.13/api...
[info] Compiling 1 Scala source to /sbttest/myplugin/target/scala-2.10/sbt-0.13/classes...
model contains 4 documentable templates
[info] Main Scala API documentation successful.
[info] Packaging /sbttest/myplugin/target/scala-2.10/sbt-0.13/my-sbt-plugin-0.1.0-javadoc.jar ...
[info] Packaging /sbttest/myplugin/target/scala-2.10/sbt-0.13/my-sbt-plugin-0.1.0.jar ...
[info] Done packaging.
[info] Done packaging.
[info] published my-sbt-plugin to /Users/freewind/.ivy2/local/test20140913/my-sbt-plugin/scala_2.10/sbt_0.13/0.1.0/poms/my-sbt-plugin.pom
[info] published my-sbt-plugin to /Users/freewind/.ivy2/local/test20140913/my-sbt-plugin/scala_2.10/sbt_0.13/0.1.0/jars/my-sbt-plugin.jar
[info] published my-sbt-plugin to /Users/freewind/.ivy2/local/test20140913/my-sbt-plugin/scala_2.10/sbt_0.13/0.1.0/srcs/my-sbt-plugin-sources.jar
[info] published my-sbt-plugin to /Users/freewind/.ivy2/local/test20140913/my-sbt-plugin/scala_2.10/sbt_0.13/0.1.0/docs/my-sbt-plugin-javadoc.jar
[info] published ivy to /Users/freewind/.ivy2/local/test20140913/my-sbt-plugin/scala_2.10/sbt_0.13/0.1.0/ivys/ivy.xml
可以看到,它顺利地把代码编译打包,并发布到了/Users/freewind/.ivy2/local/test20140913
目录下。
如果我们修改了插件代码,最好在每次调用publishLocal
前先把/Users/freewind/.ivy2/local/test20140913
清空一下,不然会有警告:
[warn] Attempting to overwrite /Users/freewind/.ivy2/local/test20140913/my-sbt-plugin/scala_2.10/sbt_0.13/0.1.0/jars/my-sbt-plugin.jar
[warn] This usage is deprecated and will be removed in sbt 1.0.
如果不清空,不知道会不会有奇怪的问题。
为了测试前面创建的plugin,我们新创建一个项目来使用它
mkdir my-project
cd my-project
touch build.sbt
mkdir project
touch project/plugins.sbt
在这个文件中引入我们的插件:
addSbtPlugin("test20140913" %% "my-sbt-plugin" % "0.1.0")
这里随便填入一些基本内容,比如:
name := "test-my-sbt-plugin"
version := "0.1.0"
需要注意的是,由于我们在MySbtPlugin
中,缺少一些必要的设置,没有自动启用插件,所以我们还必须在build.sbt
中手动启用。等我们完善MySbtPlugin
以后,下面这句话就不需要了。
lazy val root = (project in file(".")).enablePlugins(test20140913.MySbtPlugin)
注意,我们在这里可以直接调用test20140913.MySbtPlugin
,是因为该插件已经导入,里面定义的对象对于sbt配置文件来说,已经可见。
$ sbt
> plugins
In file:/sbttest/my-project/
sbt.plugins.IvyPlugin: enabled in root
sbt.plugins.JvmPlugin: enabled in root
sbt.plugins.CorePlugin: enabled in root
sbt.plugins.JUnitXmlReportPlugin: root
test20140913.MySbtPlugin: enabled in root
前面几个是sbt内置的插件。最后一行,显示我们的插件已经启动成功了!
虽然现在我们什么也做不了。
前面说了,为了使用该插件,我们需要在另一个项目中做两件事:
addSbtPlugin
引用该插件enablePlugins
启动该插件对于一个继承自AutoPlugin
的插件来说,第二步实际上可以避免的。
在MySbtPlugin.scala
中,添加以下代码:
override def trigger = allRequirements
它覆盖了父类AutoPlugin
中的trigger
方法。它有两个值可用:
allRequirements
: 当该插件依赖的其它插件满足之后,会自动启用该插件noTrigger
: 必须在项目中手动启动该插件可见第一个值就是我们需要的。(默认是第二个值)
先清空/Users/freewind/.ivy2/local/test20140913
,再重新publishLocal
删除my-project/build.sbt
中的这句代码:
lazy val root = (project in file(".")).enablePlugins(test20140913.MySbtPlugin)
其实留着也行,不过没有必要了。
为了保险起见,先删除各target
目录。然后再sbt plugins
,可以看到MySbtPlugin
依然处于启用状态。
在MySbtPlugin
中添加以下代码:
lazy val hello = taskKey[Unit]("hello task from my plugin")
val helloSetting = hello := println("Hello from my plugin")
override def projectSettings = Seq(
helloSetting
)
这里实际上有两块内容,下面依次介绍
这两行代码定义了一个简单的hello
任务:
lazy val hello = taskKey[Unit]("hello task from my plugin")
hello := println("Hello from my plugin")
前者定义了一个task key,第二行给它赋予了行为。只这么做还不够,因为它们对外不可见
override def projectSettings = Seq(
helloSetting
)
通过override projectSettings
,我们把刚定义的hello
任务加了进去,这样引用该插件的项目,会自动把这里定义的projectSettings
信息合并过去,才能使用hello
还有一个名为autoImport
的特性,我们在这里没有用上。因为它很重要,所以一起介绍。
在MySbtPlugin
的内容,我们可以定义一个名为autoImport
的object
(注意一定要是这个名字),它里面的代码将自动合并到目录项目中sbt配置中去。
比如:
object MySbtPlugin extends AutoPlugin {
object autoImport {
// here is some code will be merged to target project
// You can consider you are writing something in `build.sbt` here
}
}
再将publishLocal
,然后在my-project
中重新执行sbt
。注意各处的清理工作。
$ sbt
> plugins
In file:/sbttest/my-project/
sbt.plugins.IvyPlugin: enabled in my-project
sbt.plugins.JvmPlugin: enabled in my-project
sbt.plugins.CorePlugin: enabled in my-project
sbt.plugins.JUnitXmlReportPlugin: my-project
test20140913.MySbtPlugin: enabled in my-project
> hello
Hello from my plugin
可以看到,我们成功的调用了插件中定义的任务!直至大功告成!
可参考我的源代码:https://github.com/freewind/my-sbt-plugin