tokuhirom's Blog

groovy で ant task を記述したい

ant でロジックを記述するのはなかなか大変であるし、今となっては潰しのきかないスキルでもあり、未来ある若者に ant を使わせるべきではない。

しかし、世の中には ant を利用することを強いられている若者もいると聞く。

そういった中で、タスクを groovy でこなす方法を紹介しよう。

groovy でタスクを定義できれば、コードの可読性があがりハッピーになれるはずだ。

groovy でタスクを定義するためには groovy 本体を自動で取得する必要があるが、これは maven ant tasks で行う。maven ant tasks は、ant から maven の依存関係解決を呼べるようにしたライブラリである。

maven ant tasks はすでにインストール済みである場合も多いと思うが、入っていない場合は以下のようにすればよい。

mkdir ~/.ant/lib/
wget http://ftp.meisei-u.ac.jp/mirror/apache/dist/maven/ant-tasks/2.1.3/binaries/maven-ant-tasks-2.1.3.jar -P ~/.ant/lib/

さて、準備は整った。あとは以下のように記述すればよいだけである。 極めて簡単である。

<project name="demo" default="dist" xmlns:artifact="antlib:org.apache.maven.artifact.ant">

    <target name="dist">
        <!-- groovy-all を依存として宣言 -->
        <artifact:dependencies pathId="groovy.classpath">
            <dependency groupId="org.codehaus.groovy" artifactId="groovy-all" version="2.4.4" scope="compile"/>
        </artifact:dependencies>

        <!-- groovy-all を利用して groovy タグを有効化 -->
        <taskdef name="groovy" classname="org.codehaus.groovy.ant.Groovy" classpathref="groovy.classpath"/>

        <!-- build.groovy を実行 -->
        <groovy src="build.groovy"/>
    </target>

</project>

以上、ant から groovy を呼び出す方法を紹介した。

groovy スクリプトを実行できるようになったのはいいが、普通に groovy script で記述していくのは辛い。しかし ant から呼ばれた場合、ant の機能をつめこんだ AntBuilder といオブジェクトが渡されてくる。これを利用して、ant の機能を利用してコードを書いていけば良い。

AntBuilder についての解説はこのへんを見れば良い。 http://docs.groovy-lang.org/latest/html/documentation/ant-builder.html

良いのだが、これを直接利用するのは辛いので、ヘルパークラスを定義してこれを利用する。

以下は基本的なオペレーションをラップしている。直接 Ant を利用するのではなく、このヘルパクラスを利用して操作を行っていく。

import groovy.util.AntBuilder
import groovy.xml.NamespaceBuilder 
import org.apache.tools.ant.BuildException
import org.codehaus.groovy.ant.AntProjectPropertiesDelegate

public abstract class AbstractDeployer {
    AntBuilder ant
    AntProjectPropertiesDelegate props
    def mvn

    def AbstractDeployer(AntBuilder ant, AntProjectPropertiesDelegate props) {
        this.ant = ant
        this.props = props
        // Maven Ant Tasks
        this.mvn = NamespaceBuilder.newInstance(ant, 'antlib:org.apache.maven.artifact.ant') 
    }

    /**
     * Copy files from {@code src} to {@code dst} recursively.
     */
    def copyRecursive(String src, String dst) {
        if (new File(src).exists()) {
            ant.copy(todir:dst, overwrite:"true") {
                fileset(dir:src)
            }
        } else {
            echo("There is no ${dst}")
        }
    }

    /**
     * Get property. If there's no value for the key, this method throws Exception.
     */
    String propOrDie(String key) {
        def val = props[key]
        if (val == null) {
            throw new BuildException("No such property: '${key}'")
        }
        return val
    }

    /**
     * Get property value for {@code key}.
     */
    String prop(String key) {
        return props[key]
    }

    /**
     * Fetch {@code groupId}:{@code artifactId}:{@code version} and extract it into {@code webappOutput}.
     */
    def fetchAndUnwar(String groupId, String artifactId, String version, String webappOutput) {
        // fetch war
        mvn.dependencies(
            filesetId:'dependency.fileset.war',
            versionsId:'dependency.versions.war',
            useScope:'compile') {
            dependency(
                groupId:groupId,
                artifactId:artifactId,
                version:version,
                type:'war',
                scope:'compile')
        }

        ant.copy(todir:'target/artifacts') {
            fileset(refid:"dependency.fileset.war")
            chainedmapper {
                mapper(
                    classname:"org.apache.maven.artifact.ant.VersionMapper",
                    from:'${dependency.versions.war}')
                mapper(type:'flatten')
            }
        }

        // extract war file
        ant.unwar(
            src:"target/artifacts/${artifactId}.war",
            dest:webappOutput)
    }

    /**
     * Display {@code message}.
     */
    def echo(String message) {
        ant.echo(message:message)
    }

    /**
     * load properties from {@code srcFile}.
     */
    def loadProps(String srcFile) {
        if (new File(ant.project.baseDir, srcFile).exists()) {
            echo("loading ${srcFile}")
            ant.loadproperties(srcFile:srcFile)
        } else {
            echo("There is no ${srcFile}")
        }
    }
}

実際のビルドスクリプトは以下のような形式でやればよろしい。本来ならば、もっと DSL っぽく記述したほうがよいのだが、結局のところコンパイル時にある程度チェックできたほうが生産性が高まるので、@CompileStatic を付けられることを重視してクラスを定義している。もっと綺麗に書く方法があれば教えていただきたい。

このコードを見れば、なにがどういう手順で行われるかは一目瞭然であり、カスタマイズも簡単である。

import groovy.transform.CompileStatic
import groovy.util.AntBuilder
import org.codehaus.groovy.ant.AntProjectPropertiesDelegate

import AbstractDeployer

@CompileStatic
class Deployer extends AbstractDeployer {
    def Deployer(AntBuilder ant, AntProjectPropertiesDelegate properties) {
        super(ant, properties)
    }

    def run() {
        echo("building...")

        fetchAndUnwar("org.glassfish.admingui", 'war', '10.0-b28', 'target/webapp')
        // overwrite files
        copyRecursive("resources", "target/webapp/resources")
    }
}

new Deployer(ant, properties).run()

以上、簡単ですが ant から groovy を用いてコードを記述する方法の紹介でした。