For the longest time I’ve been writing Java build files in XML and I’ve always felt a little dirty. Not too long ago I was re-introduced to rake by a colleague (thanks Fabio) and how nicely it integrates with ant. *Headslap*

This means that you can now turn this:

<project name="spring-conversations" default="build" basedir=".">

    <property name="src.dir" location="src/main" />
    <property name="test.dir" location="src/test" />

    <property name="build.dir" location="build" />
    <property name="dist.dir" location="${build.dir}/dist" />
    <property name="report.dir" location="${build.dir}/report" />
    <property name="compile.dir" location="${build.dir}/compile" />

    <path id="classpath">
        <fileset dir="${compile.dir}" />
        <fileset dir="lib/buildtime" />
        <fileset dir="lib/runtime" />
    </path>

    <target name="build"
            depends="clean,run-tests,make-war"
            description="Main target to create WAR file." />

    <target name="run-tests"
            depends="make-jars,run-unit-tests,check-tests"
            description="Run all tests" />

    <target name="clean">
        <delete dir="${build.dir}" />
    </target>

    <macrodef name="make-jar">
        <attribute name="srcdir" />
        <attribute name="jarfile" />
        <sequential>
            <mkdir dir="${compile.dir}/classes" />
            <javac srcdir="@{srcdir}" destdir="${compile.dir}/classes" classpathref="classpath" debug="yes"
                   includeantruntime="no" />
            <jar jarfile="${compile.dir}/@{jarfile}" basedir="${compile.dir}/classes" />
            <delete dir="${compile.dir}/classes" />
        </sequential>
    </macrodef>

    <target name="make-jars">
        <make-jar srcdir="${src.dir}/java" jarfile="${ant.project.name}.jar" />
        <make-jar srcdir="${test.dir}/java" jarfile="${ant.project.name}-tests.jar" />
    </target>

    <target name="run-unit-tests">
        <mkdir dir="${report.dir}" />
        <junit fork="yes" forkmode="once" printsummary="yes" haltonfailure="no" failureproperty="tests.failed">
            <classpath location="${src.dir}/resources" />
            <classpath refid="classpath" />
            <formatter type="xml" />
            <batchtest todir="${report.dir}">
                <fileset dir="${test.dir}/java">
                    <include name="**/*Test.java" />
                </fileset>
            </batchtest>
        </junit>
    </target>

    <target name="check-tests" if="tests.failed">
        <junitreport todir="${report.dir}">
            <fileset dir="${report.dir}" includes="TEST-*.xml" />
            <report todir="${report.dir}/html" />
        </junitreport>
        <fail message="One or more tests failed. Please check the logs for more info." />
    </target>

    <target name="make-war" depends="make-jars">
        <mkdir dir="${dist.dir}" />
        <war warfile="${dist.dir}/${ant.project.name}.war" webxml="src/main/webapp/WEB-INF/web.xml">
            <fileset dir="src/main/webapp" excludes="**/web.xml" />
            <lib dir="${compile.dir}" excludes="*-tests.jar" />
            <classes dir="${src.dir}/resources" />
            <lib dir="lib/runtime" />
        </war>
    </target>

    <target name="run-jetty" depends="clean,make-jars" description="Run application in Jetty.">
        <java classname="example.jetty.WebServer" fork="true" failonerror="true">
            <classpath location="${src.dir}/resources" />
            <classpath refid="classpath" />
        </java>
    </target>

</project>

Into this:

require 'ant'

PROJECT_NAME = 'spring-conversations'

MAIN_SRC_DIR = 'src/main/java'
TEST_SRC_DIR = 'src/test/java'

RUNTIME_LIB_DIR = 'lib/runtime'
BUILDTIME_LIB_DIR = 'lib/buildtime'

BUILD_DIR = 'build'
DIST_DIR = "#{BUILD_DIR}/dist"
COMPILE_DIR = "#{BUILD_DIR}/compile"
CLASSES_DIR = "#{COMPILE_DIR}/classes"
TEST_REPORT_DIR = "#{BUILD_DIR}/report"

task :default => [:clean, :run_tests, :make_war]

task :clean do
  ant.delete :dir => BUILD_DIR
  puts
end

task :setup do
  ant.path :id => 'classpath' do
    fileset :dir => COMPILE_DIR
    fileset :dir => RUNTIME_LIB_DIR
    fileset :dir => BUILDTIME_LIB_DIR
  end
end

task :make_jars => :setup do
  make_jar MAIN_SRC_DIR, "#{PROJECT_NAME}.jar"
  make_jar TEST_SRC_DIR, "#{PROJECT_NAME}-tests.jar"
end

def make_jar(source_folder, jar_file_name)
  ant.mkdir :dir => CLASSES_DIR
  ant.javac :srcdir => source_folder, :destdir => CLASSES_DIR, :classpathref => 'classpath',
            :source => "1.6", :target => "1.6", :debug => "yes", :includeantruntime => "no"
  ant.jar :jarfile => "#{COMPILE_DIR}/#{jar_file_name}", :basedir => CLASSES_DIR
  ant.delete :dir => CLASSES_DIR
  puts
end

task :run_tests => :make_jars do
  ant.mkdir :dir => TEST_REPORT_DIR
  ant.junit :fork => "yes", :forkmode => "once", :printsummary => "yes",
            :haltonfailure => "no", :failureproperty => "tests.failed" do
    classpath :refid => 'classpath'
    formatter :type => "xml"
    batchtest :todir => TEST_REPORT_DIR do
      fileset :dir => TEST_SRC_DIR, :includes => '**/*Test.java'
    end
  end
  if ant.project.getProperty("tests.failed")
    ant.junitreport :todir => TEST_REPORT_DIR do
      fileset :dir => TEST_REPORT_DIR, :includes => "TEST-*.xml"
      report :todir => "#{TEST_REPORT_DIR}/html"
    end
    ant.fail :message => "One or more tests failed. Please check the test report for more info."
  end
  puts
end

task :make_war => :make_jars do
  ant.mkdir :dir => DIST_DIR
  ant.war :warfile => "#{DIST_DIR}/#{PROJECT_NAME}.war", :webxml => "src/main/webapp/WEB-INF/web.xml" do
    fileset :dir => "src/main/webapp", :excludes => "**/web.xml"
    lib :dir => COMPILE_DIR, :excludes => "*-tests.jar"
    classes :dir => "src/main/resources"
    lib :dir => RUNTIME_LIB_DIR
  end
  puts
end

task :run_jetty => [:clean, :make_jars] do
  ant.java :classname => "example.jetty.WebServer", :fork => 'yes', :failonerror => 'yes' do
    classpath :location => "src/main/resources"
    classpath :refid => "classpath"
  end
end

Damn, this feels like code.