Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2014-09-13) 如何创建一个简单的sbt插件

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

经过几个小时的吐血,踩了无数的坑以后,终于成功的创建一个简单的sbt插件。其功能异常简单:使用该插件后,可以执行任务hello,输出一段文字。

创建一个普通的sbt项目

一个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版本

在sbt 0.13.5中,sbt提供了一种新的插件机制AutoPlugin,可以让插件自动被sbt发现并载入,不再像以前那样必须在build.sbt里添加一些初始化代码。

我们的插件将使用这种方式,所以我们需要指定sbt版本。

可以在project/build.properties中填入以下内容:

sbt.version=0.13.5

或者,直接在build.sbt里添加:

sbtVersion := 0.13.5

sbtPlugin := true

build.sbt中添加:

sbtPlugin := true

它将自动将sbt作为依赖添加到我们项目中,方便我们在代码中引用sbt中定义的类。

定义MySbtPlugin,继承AutoPlugin

我们需要创建一个继承自AutoPluginobject,名字可以随便取,这里我使用MySbtPlugin。它是插件的入口。

放在哪里

这个MySbtPlugin.scala应该放在哪里呢?

  1. 可以直接放在项目根目录下,如果比较简单的话
  2. 放在src/main/scala/

这两个位置都是sbt可以自动发现、编译和处理的。

不能放在project下,因为这里的scala文件都是为了配置sbt的构建命令而存在的,如果放在这里,它们不会被发布出去。

package

需要放在一个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的功能呢?我们晚点在后面实现。

我们可以先把它发布到本地试试,看看效果。

发布

需要使用sbtpublicLocal命令,把它发布到本地。

更新sbt到0.13.5或以上

因为我们用到的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也发布了,应该很快能更新。

publishLocal

$ 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

为了测试前面创建的plugin,我们新创建一个项目来使用它

创建新项目

mkdir my-project
cd my-project

touch build.sbt

mkdir project
touch project/plugins.sbt

project/plugins.sbt

在这个文件中引入我们的插件:

addSbtPlugin("test20140913" %% "my-sbt-plugin" % "0.1.0")

build.sbt

这里随便填入一些基本内容,比如:

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内置的插件。最后一行,显示我们的插件已经启动成功了!

虽然现在我们什么也做不了。

让我们的插件自动启用

前面说了,为了使用该插件,我们需要在另一个项目中做两件事:

  1. project/plugins.sbt中通过addSbtPlugin引用该插件
  2. build.sbt中通过enablePlugins启动该插件

对于一个继承自AutoPlugin的插件来说,第二步实际上可以避免的。

在MySbtPlugin中设置trigger

MySbtPlugin.scala中,添加以下代码:

override def trigger = allRequirements

它覆盖了父类AutoPlugin中的trigger方法。它有两个值可用:

  1. allRequirements: 当该插件依赖的其它插件满足之后,会自动启用该插件
  2. noTrigger: 必须在项目中手动启动该插件

可见第一个值就是我们需要的。(默认是第二个值)

重新发布

先清空/Users/freewind/.ivy2/local/test20140913,再重新publishLocal

删除my-project中build.sbt中的enablePlugins

删除my-project/build.sbt中的这句代码:

lazy val root = (project in file(".")).enablePlugins(test20140913.MySbtPlugin)

其实留着也行,不过没有必要了。

重新运行

为了保险起见,先删除各target目录。然后再sbt plugins,可以看到MySbtPlugin依然处于启用状态。

添加hello任务

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 task

这两行代码定义了一个简单的hello任务:

lazy val hello = taskKey[Unit]("hello task from my plugin")
hello := println("Hello from my plugin")

前者定义了一个task key,第二行给它赋予了行为。只这么做还不够,因为它们对外不可见

添加到projectSettings中

  override def projectSettings = Seq(
    helloSetting
  )

通过override projectSettings,我们把刚定义的hello任务加了进去,这样引用该插件的项目,会自动把这里定义的projectSettings信息合并过去,才能使用hello

autoImport

还有一个名为autoImport的特性,我们在这里没有用上。因为它很重要,所以一起介绍。

MySbtPlugin的内容,我们可以定义一个名为autoImportobject(注意一定要是这个名字),它里面的代码将自动合并到目录项目中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

comments powered by Disqus