Monday, May 21, 2007

Avoid Copy & Paste with Ant MacroDef

Keywords:
ant java copy paste macrodef gwt compile task

Problem:
There's no convenient ant task to allow you to compile (or re-compile) the GWT client files, but you can get some decent up-to-date check & compile behaviour going with a few lines of ant

... problem is, you need to duplicate these lines of code for every module in the project. It would be nice if you could modularise this.

Solution:
The Ant "macrodef" task lets you define a sequence of ant steps in a parameterised way and then call it any number of times with different parameters.

As can be seen in the example below (which handles 2 modules), adding a new module will require:
  1. add a new entry to the "depends" list for gwt-generate (eg 'gwt-generate-newmod')
  2. create a gwt-compile target 'gwt-generate-newmod' with an "unless" attribute of 'gwtGenerate.newmod.notRequired'
  3. create a gwt-uptodate target to set the property 'gwtGenerate.newmod.notRequired' accordingly.
Yes, there's still some copy and paste, but you're just defining the module name and location rather that repeating the gwt-compile definition so there's less chance for a mistake.
<!-- this is the root target which will do all the generation (if required) -->
<target name="gwt-generate" description="Generate GWT code"
  depends="gwt-generate-common, gwt-generate-demo"/>

<target name="gwt-generate-common" depends="compile, gwt-check" unless="gwtGenerate.common.notRequired">
    <gwt-compile module="com.example.gwt.common.MyCommonModule"/>
</target>
<target name="gwt-generate-demo" depends="compile, gwt-check" unless="gwtGenerate.demo.notRequired">
    <gwt-compile module="com.example.gwt.demo.MyDemoModule"/>
</target>

<!-- this sets the *.notRequired properties if gwt code is uptodate -->
<target name="gwt-check">
  <!-- won't have any effect if the dirs are already there -->
  <mkdir dir="${build.home}/gwt"/>
  <mkdir dir="${build.home}/gwt/tmp"/>
  <mkdir dir="${build.home}/gwt/out"/>

    <gwt-uptodate property="gwtGenerate.common.notRequired"
            module="com.example.gwt.common.MyCommonModule"
            package="src/com/example/gwt/common"/>

    <gwt-uptodate property="gwtGenerate.demo.notRequired"
                    module="com.example.gwt.demo.MyDemoModule"
                    package="src/com/example/gwt/demo"/>          
</target>

<!-- this defines a macro task 'gwt-uptodate' -->
<macrodef name="gwt-uptodate" description="sets a property indicating if the module is up to date">
 <attribute name="property"/>
 <attribute name="module"/>
 <attribute name="package"/>
 <sequential>
         <!-- the module is uptodate if the .nocache.html is not older than any of the module src files -->
      <uptodate property="@{property}"
              targetfile="${build.home}/gwt/out/@{module}/@{module}.nocache.html">
          <srcfiles dir="@{package}" includes="**/*.*"/>
      </uptodate>                      
 </sequential>
</macrodef>  
<!-- this defines a macro task 'gwt-compile' -->
<macrodef name="gwt-compile" description="generates the GWT client code">
 <attribute name="module"/>
 <sequential>
         <!-- you must fork or it will fail -->
         <java dir="${build.home}/gwt/tmp" classname="com.google.gwt.dev.GWTCompiler" fork="true">
          <classpath>
              <!-- src directory containing module definition must be first! -->
              <pathelement location="src"/>
              <pathelement location="${build.home}/classes"/>
              <pathelement location="${gwt.user}"/>
              <pathelement location="${gwt.dev}"/>
              <pathelement location="${gwt.widgets}"/>
            </classpath>
          
          <sysproperty key="java.awt.headless" value="true"/>             
          <arg value="-out"/>
          <arg value="../out"/>
          <arg value="@{module}"/>  
      </java>
 </sequential>
</macrodef>


Notes:
Note, you only need to GWT-compile modules with an EntryPoint class in them. This is because it's only EntryPoint classes that you need to reference from your HTML and on compilation all the classes (and files in com.example.gwt.package/public!) are compiled/copied to this module. This means if you have multiple EntryPoint modules in your project (as I do) that reference a common Module, they will have their own compiled copy of this (and the /public files) rather than referencing it in a common location.

Note also, this means that the gwt-uptodate won't trigger a recompile if there's a change to a referenced module - as it's only checking the source folder containing the EntryPoint module ... so in those cases, you'll need to do a clean of the build folder containing generated source to guarantee everything is compiled up to date.

New Note: the sysproperty to set awt to headless is crucial on linux (if you're using ImageBundles). See Does the GWT 1.4 Compiler Need an X11 Window in Linux?

No comments: