GeekFactory

int128.hatenablog.com

今日から始めるGradleプラグイン開発 #gadvent

G*Advent Calendar(Groovy,Grails,Gradle,Spock...) Advent Calendar 2014 - Qiitaの17日目です。

本記事では、9日目の記事で紹介したGradleプラグインのテンプレートプロジェクトを使って、新しいGradleプラグインを作る方法を説明します。14日目の記事で紹介したSlashプラグインを題材にして、実際にGradleプラグインを作っていきます。

準備

Gradleプラグインを開発するにはJVMが必要です。IDEはなくても開発できますが、あった方が便利です。ここでは、IntelliJ IDEAを使う前提とします。

適当なフォルダにGradleプラグインのテンプレートプロジェクトを配置します。

git clone https://github.com/int128/gradle-plugin-blank.git gradle-slash
cd gradle-slash
git remote remove origin

まずは、コマンドラインでビルドしてみましょう。テンプレートプロジェクトにはGradle wrapperを同梱しているので、Gradleをインストールする必要はありません。以下のコマンドでビルドを実行できます。

./gradlew build

BUILD SUCCESSFULと表示されれば成功です。

次に、IDEAでプロジェクトを開いてみましょう。最初の画面でImportを選び、build.gradleを開きます。com.exampleのパッケージツリーが表示されれば成功です。

プラグインの名付け

GradleプラグインにはユニークなIDを付ける必要があります。プラグインIDは、ビルドスクリプトapply pluginプラグインを適用する際に指定するIDになります。Javaのパッケージ規則と同様に、自分が所有しているドメインGitHubアカウントにするとよいでしょう。詳細は、公式の how to submit your plugin を参照してください。

プラグインIDを決めたら gradle.properties にある以下の項目を修正します。

  • gradle.plugin.id - プラグインID。例:org.hidetake.slash
  • group - グループ名。例:org.hidetake
  • description - プラグインの説明。

ここで、プラグインコードを自動生成します。テンプレートプロジェクトには、プラグインコードを自動生成するスクリプトを同梱しています。以下のように generatePlugin タスクを実行します。

./gradlew generatePlugin

:generatePluginClass
Generating plugin class: .../src/main/groovy/org/hidetake/SlashPlugin.groovy
:generatePluginMetadata
Generating plugin metadata: .../src/main/resources/META-INF/gradle-plugins/org.hidetake.slash.properties
:generatePluginTestClass
Generating plugin test: .../src/test/groovy/org/hidetake/SlashPluginSpec.groovy
:generatePlugin

BUILD SUCCESSFUL

テンプレートプロジェクトにはあらかじめHelloPluginというサンプルが入っているので、削除しておきます。

プラグインの開発

プラグインの開発は以下のステップで行うと捗ります。いわゆるREADME駆動開発です。

  1. READMEを書く
  2. 受け入れテストを書く
  3. Spockのテストコードを書く
  4. プロダクトコードを書く

1. READMEを書く

プラグインの特長を README.md に書きます。仕様を詳細に書く必要はありません。ユーザはなぜこのプラグインを使いたくなるのか?どんな嬉しいことがあるのか?という視点でREADMEを書きます。

最初にREADMEを書くことで、ユーザ視点で仕様を考えるための準備が整います。たかがGradleプラグインのためにと思うかもしれませんが、ユーザに価値あるプラグインを提供するためにREADMEは重要な役割を果たすのです。

2. 受け入れテストを書く

ここでは、ユーザがGradle上で実際にプラグインを適用した場合の受け入れ条件のチェックを「受け入れテスト」と呼ぶことにします。受け入れテストですべての仕様を網羅する必要はありません。どちらかというとプラグインの使い方を説明する目的で記述するとよいでしょう。

受け入れテストには、ユーザ視点でプラグインの使い方を考える目的に加えて、実際にGradleでプラグインを適用してみないと分からない問題に対処する目的があります。後述するSpockのテストコードにはimport文や変数宣言があるため、実際のビルドスクリプトと動作条件が異なる場合があります。また、他のプラグインと組み合わせた場合の挙動は実際にビルドスクリプトを実行してみないと分かりません。

受け入れテストのコードは acceptance-test/build.gradle に記述します。

Slashプラグインでは以下を受け入れテストとしています。これは単純すぎるのであまりありがたみを感じませんが、Gradle SSH Pluginではもっと複雑な受け入れテストを行っています。

apply plugin: 'org.hidetake.slash'

task test << {
    assert buildDir / 'tmp' == file("$buildDir/tmp")
    assert projectDir / 'build' == buildDir
    assert projectDir / 'build' / 'generated' == buildDir / 'generated'
}

受け入れテストは以下のコマンドで実行します。具体的には、ローカルのMavenリポジトリにSNAPSHOTバージョンのプラグインをインストールした上で、受け入れテストプロジェクトを実行します。

./gradlew install
./gradlew -p acceptance-test test

なお、gradlewに -Pversion=0.1 のようにバージョンを渡して実行することで、リリース済みのプラグインに対するテストも実行できます。

3. テストコードを書く

Spockでテストコードを書きます。テストコードは src/test/groovy に配置します。ここでは、プラグインの仕様テストと、プラグインが依存するドメインクラスの仕様テストを記述します。

典型的なテストコードは、プロジェクトにプラグインを適用して、実行に対する事後条件をチェックする流れです。以下に例を示します。

    def "apply() should load the plugin"() {
        given:
        def project = ProjectBuilder.builder().build()

        when:
        project.with {
            apply plugin: 'com.example.hello'
        }

        then:
        project.plugins.hasPlugin(HelloPlugin)
    }

なお、プラグインが単純な場合は、先ほどの受け入れテストを省略して、Spockのテストケースだけにしても構いません。

4. プロダクトコードを書く

プロダクトコードはGroovyやJavaで記述します。Gradle API自体はJavaで書かれているのでプラグインJavaで書けますが、開発効率を考えるとGroovyがおすすめです。

プラグインのエントリポイントは src/main/resources/META-INF/gradle-plugins/*.properties に書いてあるクラスです。例えば、Slashプラグインの場合は以下の動作になります。

  1. ビルドスクリプトapply plugin: 'org.hidetake.slash' が実行される
  2. プラグインJARを検索して META-INF/gradle-plugins/org.hidetake.slash.properties を探す
  3. META-INF/gradle-plugins/org.hidetake.slash.properties に書かれているクラスをロードする
  4. 当該クラスをインスタンス化して、apply メソッドを実行する

Slashプラグインの中心部分を抜き出すと以下になります。ここではFileクラスのメタクラスを操作して除算演算子を追加しています。

class SlashPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        File.metaClass.mixin(PathCategory)
    }
}

@Category(File)
class PathCategory {
    File div(String child) {
        new File(this as File, child)
    }
}

プラグインでは、プロジェクトに新しいプロパティを追加したり、ビルド前後のフックを追加したりできます。ただし、プロジェクトを無秩序に拡張することはグローバル汚染につながります。このため、プロジェクトを拡張するには project.conventions ではなく project.extensions を使うことが推奨されています

プラグインのコードを書くにあたっては、Gradle DSLのドキュメントが役に立ちます。また、Gradle Plugin Portalで公開されているプラグインのコードが参考になります。

これまで述べた一連の流れを繰り返すことで、プラグインを開発していきます。

プラグインの公開

プラグインを他の人に使ってもらうには、Maven CentralやBintrayなどのリポジトリにリリースする必要があります。プラグインはどこにリリースしてもよいのですが、Gradle Plugin Portalに掲載するにはBintrayにリリースする必要があります。本記事ではBintrayにリリースする前提で話を進めます。

まず、Bintrayにサインアップします。GitHubアカウントで認証できます。

ここでは、初期状態で用意されているmavenリポジトリの下に新しいパッケージを作成します。パッケージ名はGradleのプロジェクト名と同じにします。

最後にAPIキーを取得します。APIキーはユーザ設定のページから取得できます。

ローカルマシンからBintrayにリリースする

ユーザ名とAPIキーをコロンで連結した文字列をローカルマシンの ~/.gradle/gradle.properties に書きます。

bintray.credential=user:apikey

その後、Bintrayにアップロードするタスクを実行します。

./gradlew -Pversion=0.1 bintrayUpload

Bintrayにアクセスしてアップロードされたファイルを確認します。問題なければ Publish をクリックして公開します。なお、ビルドスクリプトの設定を変更すれば Publish の操作を省略することも可能です。

Travis CIからBintrayにリリースする

プロダクトをリリースする際は、コミットにタグを付けることが多いと思います。タグを付けたタイミングで自動的にリリースが実行されると便利ですよね。そこで、GitHubにタグがpushされたタイミングでTravis CIがリリースを実行する仕組みを実現してみましょう。

Bintrayのユーザ名とAPIキーを連結した文字列を .travis.yml に追記します。以下のコマンドで暗号化しておきます。

travis encrypt --add -- BINTRAY=user:apikey

.travis.yml を変更したらコミットしてpushしておきましょう。これで準備は完了です。

vから始まるタグをリモートリポジトリにpushします。

git tag v0.1
git push origin --tags

Travis CIにアクセスしてログを確認します。ビルドの完了後、Bintrayにプラグインがアップロードされていれば成功です。

まとめ

Gradleプラグインのテンプレートプロジェクトを使ったGradleプラグインの作り方を駆け足で紹介しました。

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築