継続的インテグレーションツール Hudson のプラグインを作成

CI(継続的インテグレーション)ツールの一つである Hudson は以下のような点で個人的に気に入っている。

  • インストール(実行環境の構築や設定など)が容易
  • プラグインによる機能追加が容易
  • ビルド結果等のファイル構成が非常にシンプル
  • REST 的なリモートアクセス API が用意(XML、JSON)

Apache Tomcat の webapps ディレクトリにダウンロードした hudson.war ファイルを配置して Tomcat を起動するだけで使えるし、hudson の起動時に作成された .hudson ディレクトリの plugins ディレクトリにプラグイン(.hpi ファイル)を配置して Tomcat を再起動するだけでプラグインが使えるようになるなど、手間がかからず手軽に使い始められる。

今回は Hudson プラグインの自作に興味がわいたので、Extend Hudsonのサイトを参考にして実際に作ってみた。

事前準備

まず、hudson のプラグイン作成には Maven を使用するため、Apache Maven 2 をダウンロードしインストールしておく。(今回は Maven 2.0.9 を使用)

次に、下記 URL から pom.xml ファイルをダウンロードし、適当なディレクトリに配置する。

mvn package を実行し hudson プラグイン作成に必要なライブラリを Maven のローカルリポジトリにダウンロードする。

>mvn package

mvn package が正常に終了すれば、pom.xml ファイルは不要なため削除。

プラグイン作成プロジェクトの作成

hudson プラグインを作成するためのディレクトリを用意、その中で以下のコマンドを実行して、hudson のプラグイン作成用プロジェクトを作成する。

>mvn org.jvnet.hudson.tools:maven-hpi-plugin:1.20:create

作成処理の最後の方で、groupId(パッケージ名)と artifactId(プロジェクト名)の入力が促されるので適当に入力する。

なお、上記コマンドの 1.20 の部分は、Maven2 のローカルリポジトリのライブラリ内容に合わせて適時修正することになると思う。

Eclipse にインポートする場合

Eclipse で開発を実施する場合、以下のコマンドを実行して Eclipse 用のプロジェクトファイル(.project や .classpath)を生成し、Eclipse にインポートする。

>mvn -DdownloadSources=true eclipse:eclipse

.classpath ファイルでは、Maven2 のローカルリポジトリの場所を環境変数 M2_REPO で指定しているため、eclipse環境変数で M2_REPO が未設定であれば、.m2/repository ディレクトリへのパスを設定する。

サンプルプラグインの構成

create で生成されたプロジェクトには、既に HelloWorldBuilder というサンプルプラグインが生成されており、これを mvn package して .hudson/plugins に配置すればプラグインとして動作するようになっているので、プラグイン作成の参考になると思う。。

ちなみに、主なファイル構成は以下の通り。

  • pom.xml ファイル
  • src/main/java/パッケージ名 ディレクトリ
    • HelloWorldBuilder.java ファイル : プラグインの実装クラス、Builder 抽象クラスを継承している
    • PluginImpl.java ファイル : プラグインのエントリーポイントを実装するクラス(1プラグインに 1クラス必要)
  • src/main/resources/パッケージ名/HelloWorldBuilder ディレクトリ
    • global.jelly ファイル : 「Hudsonの管理」->「システムの設定」で表示・設定する内容を記述
    • config.jelly ファイル : プロジェクト毎の「設定」で表示・設定する内容を記述

拡張ポイント(Extension points)

プラグインは拡張ポイントを使って実装するような仕組みになっており、以下のような拡張ポイントが用意されている。

  • Publisher
    • プロジェクト「設定」の「ビルド後の処理」で選択対象になる
  • Builder
    • プロジェクト「設定」の「ビルド」で選択対象になる
  • SCM
    • プロジェクト「設定」の「ソースコード管理システム」で選択対象になる
  • Trigger
    • プロジェクト「設定」の「ビルド・トリガ」で選択対象になる
  • Action
    • ビルド結果の画面に表示される(左メニューに追加されたりする)
  • Job
  • MavenReporter
  • BuildWrapper

Publisher 拡張ポイントを使ったプラグインの作成

ここから、本格的にプラグインを作成していくことにするが、サンプルプラグインの HelloWorldBuilder を改造するだけでは面白みに欠けるので、別の拡張ポイント Publisher を使ったプラグインを作成していくことにする。

とりあえず、多少の実用性を考慮して以下のような機能を実装することにした。

  • JavaNCSS によるソフトウェアメトリクスの測定結果(XMLファイル)を Hudson 上で表示

なお、今回は表示メッセージなどの国際化は考慮しない事とする。

ちなみに、ソフトウェアメトリクスのツールとして JavaNCSS を選んだ理由は、Maven2 用のプラグインが用意されていたからで、下記のように Maven2 を使用して容易にメトリクスの測定が可能となっている点がポイントだった。

Maven2用 JavaNCSS プラグインの使い方

以下のように、メトリクスの測定対象プロジェクト(Maven2 用のプロジェクト)の pom.xml ファイルに JavaNCSS プラグインの定義を追加して、mvn javancss:report を実行すれば(mvn site でも可)、target/javancss-raw-report.xml ファイルにソフトウェアメトリクスの測定結果が出力される。

<project>
  ・・・
  <reporting>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>javancss-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </reporting>
  ・・・
</project>
>mvn javancss:report
もしくは
>mvn site

それでは本題に戻って、Hudson のプラグインを作成していく事にする。今回は groupId に simple、artifactId に simpleTest という名称を使った。

まず、サンプルクラス HelloWorldBuilder.java の名称を変更して拡張ポイント Publisher を継承するクラスを以下のように作成。

src/main/java/simple/JavaNcssPublisher.java ファイル
package simple;

import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Result;
import hudson.tasks.Publisher;

import java.io.IOException;

import org.kohsuke.stapler.StaplerRequest;

public class JavaNcssPublisher extends Publisher {

    JavaNcssPublisher() {
    }

    //ビルド後に実施する処理を記述
    @Override
    public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {

        FilePath[] files = build.getProject().getWorkspace().list("**/javancss*report.xml");

        if (files.length == 0) {
            listener.getLogger().println("not found : javancss report file");
            build.setResult(Result.FAILURE);
        }
        else {
            FilePath root = new FilePath(build.getRootDir());
            FilePath target = new FilePath(root, "javancss_result.xml");

            //workspace 内に生成された javancss-raw-report.xml ファイルを
            //ビルドごとに生成されるディレクトリに
            //javancss_result.xml という名称でコピー
            files[0].copyTo(target);

            JavaNcssAction act = new JavaNcssAction(build);
            act.setResultFileName(target.getName());

            //ビルド結果に JavaNcssAction インスタンスを追加。
            //ビルド結果の構成を保存する build.xml ファイルに
            //シリアライズされて保存される
            build.addAction(act);
        }

        return true;
    }

    public Descriptor<Publisher> getDescriptor() {
        return DESCRIPTOR;
    }

    public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

    public static final class DescriptorImpl extends Descriptor<Publisher> {
        DescriptorImpl() {
            super(JavaNcssPublisher.class);
        }

        public String getDisplayName() {
            return "JavaNCSS reports";
        }

        public JavaNcssPublisher newInstance(StaplerRequest req) throws FormException {
            return new JavaNcssPublisher();
        }
    }
}

拡張ポイント Publisher を使用するため、hudson.tasks.Publisher を継承し perform メソッドに Publisher としてビルド後に実施する処理を実装する。

Descriptor 関係の処理は、HelloWorldBuilder.java ファイルの処理を元にクラス名の箇所を書き直す。

次に、PluginImpl.java を以下のように修正。

src/main/java/simple/PluginImpl.java ファイル
package simple;

import hudson.Plugin;
import hudson.tasks.BuildStep;

public class PluginImpl extends Plugin {
    public void start() throws Exception {
        BuildStep.PUBLISHERS.add(JavaNcssPublisher.DESCRIPTOR);
    }
}

BuildStep.PUBLISHERS に JavaNcssPublisher.DESCRIPTOR を追加するように変更。

なお、PluginImpl クラスはプラグインファイル(.hpi ファイル)内の META-INF/MANIFEST.MF ファイルの Plugin-Class として設定され、プラグインを指定の拡張ポイントに登録する処理を実行する。

JavaNcssAction クラスの実装は後回しにして、先に「Hudsonの管理」->「システムの設定」画面で表示するビューを作成するために、resources ディレクトリの HelloWorldBuilder ディレクトリを JavaNcssPublisher に名称変更して、global.jelly を以下のように変更。

src/main/resources/simple/JavaNcssPublisher/global.jelly ファイル
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">

  <f:section title="JavaNCSS Publisher">
  </f:section>

</j:jelly>

今回は設定値の入力が不要なので、とりあえずプラグインの項目名だけを表示するようにした。

更に、ジョブの「設定」画面で表示するビューを作成するために、config.jelly を以下のように変更。

src/main/resources/simple/JavaNcssPublisher/config.jelly ファイル
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
</j:jelly>

設定値の入力が不要なので何も記述しなかった。これにより、「設定」->「ビルド後の処理」において、JavaNcssPublisher で実装した DescriptorImpl クラス getDisplayName() の戻り値を伴ったチェックボックスだけが表示されるようになる。

次に、Action インターフェースを実装した JavaNcssAction クラスを作成する。

主要なメソッドの説明は以下のとおり。

  • getDisplayName() メソッドの戻り値はビルド画面の左メニューに表示
  • getIconFileName() メソッドの戻り値はビルド画面の左メニューにアイコンとして表示
    • 戻り値が null の場合は項目自体が非表示になるので注意が必要
    • / で区切らないファイル名を指定すると Hudson 内のメインリソースを参照する模様
  • getUrlName() メソッドの戻り値は URL に組み込まれる
    • "/job/ジョブ名/ビルド番号/getUrlName()の戻り値" という URL が生成
src/main/java/simple/JavaNcssAction.java ファイル
package simple;

import hudson.model.AbstractBuild;
import hudson.model.Action;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.XPath;
import org.dom4j.io.SAXReader;

public class JavaNcssAction implements Action {

    private static final long serialVersionUID = 1L;

    private AbstractBuild<?, ?> owner;
    private String resultFileName;

    public JavaNcssAction(AbstractBuild<?, ?> owner) {
        this.owner = owner;
    }

    @Override
    public String getDisplayName() {
        return "JavaNCSS Results";
    }

    @Override
    public String getIconFileName() {
        return "document.gif";
    }

    @Override
    public String getUrlName() {
        return "javaNcss";
    }

    public AbstractBuild<?, ?> getOwner() {
        return this.owner;
    }

    public List<FunctionMetrics> getFunctionMetricsList() {
        return this.parseResultXml();
    }

    public void setResultFileName(String resultFileName) {
        this.resultFileName = resultFileName;
    }

    //javancss_result.xml ファイルを解析して、
    //メソッドに関するメトリクス結果を List に格納
    private List<FunctionMetrics> parseResultXml() {
        List<FunctionMetrics> result = new ArrayList<FunctionMetrics>();

        if (this.resultFileName != null) {

            SAXReader saxReader = new SAXReader();

            try {
                Document doc = saxReader.read(new File(this.owner.getRootDir(), this.resultFileName));
                XPath xpath = DocumentHelper.createXPath("//function");

                for (Element func : (List<Element>)xpath.selectNodes(doc)) {
                    FunctionMetrics fm = new FunctionMetrics();

                    fm.name = func.elementTextTrim("name");
                    fm.ccn = this.parseInt(func.elementTextTrim("ccn"));
                    fm.ncss = this.parseInt(func.elementTextTrim("ncss"));

                    result.add(fm);
                }
            } catch (DocumentException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    private Integer parseInt(String number) {
        Integer result = null;
        
        if (number != null) {
            try {
                result = Integer.parseInt(number);
            }
            catch (NumberFormatException ex) {
            }
        }

        return result;
    }

    //メソッドのメトリクス結果を格納するためのクラス
    public class FunctionMetrics {
        public String name;
        public Integer ncss;
        public Integer ccn;
    }
}

なお、JavaNcssAction クラスは、URLが "/job/ジョブ名/ジョブ番号/javaNcss" の場合に適用されるようになり、ビューとして simple/JavaNcssAction/index.jelly ファイルが使用されるが、別の URL にリダイレクトしたり、index.jelly の適用前に処理を実施する場合は、URL の内容に基づいた doXXX メソッドや doDynamic メソッドを定義して処理を実装することになる。

最後にソフトウェアメトリクスの結果表示画面を作成するために resources/simple/JavaNcssAction ディレクトリを作成し、index.jelly ファイルを下記のような内容で作成する。

src/main/resources/simple/JavaNcssAction/index.jelly ファイル
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">

  <l:layout>
    <st:include it="${it.owner}" page="sidepanel.jelly" />
    <l:main-panel>
      <h1>JavaNCSS Results</h1>

      <table>
        <tr>
          <th>Method Name</th>
          <th>NCSS</th>
          <th>CCN</th>
        </tr>
        <j:forEach var="fm" items="${it.functionMetricsList}">
          <tr>
            <td>${fm.name}</td>
            <td>${fm.ncss}</td>
            <td>${fm.ccn}</td>
          </tr>
        </j:forEach>
      </table>

    </l:main-panel>
  </l:layout>

</j:jelly>

jelly ファイル内では it で JavaNcssAction のインスタンスを参照可能。ここではメソッドに関するメトリクス結果を JavaNcssAction.getFunctionMetricsList() から取得し、table で表示するようにしている。

sidepanel.jelly の設定は、左メニューの表示を保つための措置。

ちなみに、NCSS はコメント行を除く行数、CCN はサイクロマチック複雑度という測定項目。

プラグインの配置と実行

mvn package の実行で生成された simpleTest.hpi を .hudson/plugins ディレクトリに配置して Hudson を再起動する。(通常、.hudson はユーザーのホームディレクトリに作成される)

mvn package

以下のような手順で、JavaNCSS でソフトウェアメトリクスを実施するジョブを作成し、ビルドを実行する。

  1. 「新規ジョブ作成」でジョブを作成
  2. ソースコード管理システム」の設定を実施
  3. 「ビルド」の「Maven の呼び出し」のゴールで package と javancss:report を設定(pom.xml ファイルに Maven2 JavaNCSS プラグインの設定を施しておく)
  4. 「ビルド後の処理」で「JavaNCSS reports」にチェックをつける

ビルド画面から「JavaNCSS Results」に移動して、メトリクス結果画面を表示すると以下のような画面が表示される。(今回は、このプラグイン作成プロジェクトのソースコード自体を Hudson のジョブに設定)