Tuesday, June 21, 2011

Building a WPF Project with Ant

Generally speaking, I never endorse deploying an application to a production environment unless it has been built from scratch in a clean environment. This means that building a portlet factory WAR from the eclipse IDE is not the way to do a production build, even though it is really convenient.

Building with Ant

Apache ant has been around for a long while now, is an excellent framework for building WAR files and is the tool that WPF uses to build its own WAR files. In older versions of WPF the build process was somewhat lacking, but the team at IBM has really done a great job with the latest release of the product (I think this improved build may have been available in 6.1.5, but we skipped over that and went straight to 7.0).
Below is an example of how I've taken the sample build.xml provided with the product and modified it to pull code from CVS and allow for project specific settings.

The build.properties is mostly unchanged:
CVSTag=HEAD

# Build source root directory !NOTE! This value must be an absolute path.
buildsrc.location=C:/wsad/GCPS_V7/PF_Build/WPF7/${project.name}

# Build temp location. This is where the temporary project will be created for building purposes
buildtmp.location=${buildsrc.location}/buildtmp

# The root directory for the Portlet Factory files.
wpf.artifacts.dir=C:/wsad/GCPS_V7/PF_Build/WPF7

# The output location for the generated war files.
builddist.location=${buildsrc.location}/dist

# The location of the pbportlet.jar file. This ships with portal
c2a.lib.dir=${wpf.artifacts.dir}/WebSphere Portal

# The name of the project (used for identifiying war files)
project.name=SET PROJECT NAME IN XML BUILD FILE

#-- The following properties should not need to be changed under normal circumstances. --

# The root directory of the temp project. This is used by projectDeploy.xml
project.location=${buildtmp.location}

# The WebContent directory of the project that will be created. This is also used by projectDeploy.xml
webcontent.location=${project.location}/WebContent

# The output directory for standalone deployment wars.
build.deployment.war.builddir=${builddist.location}

# The output directory for the portlet wars
wpf.portletwar.location=${builddist.location}

# The shipping version of Portlet Factory
version=7.0.0

Note that I've added a new property CVSTag which is used to identify which label or branch to use with CVS. I've also changed the project.name property to a self descriptive value that will stand out during a build if it is not changed. The intent is that this build.properties file does not contain any project specific values - those will be set elsewhere.

Next, I've taken the sample build.xml and made a few changes to it as well:
<project name="build" default="build4GCPS" basedir=".">

  <!-- define build script properties. -->
  <target name="properties" depends="propertiesOverride">  
    <property file="build.properties" />
  </target>

  <target name="build4GCPS" depends="properties, clean">    

    <!-- create temp directory structure for building -->
    <mkdir dir="${builddist.location}" />
    <mkdir dir="${buildtmp.location}" />  

    <cvs package="${project.name}" cvsroot=":pserver:blduser:XXXX@cvserver1:/usr/cvs/projects" tag="${CVSTag}"/>

    <!-- create temp dir to build on top of -->
    <copy todir="${buildtmp.location}/.deployment">
      <fileset dir="${buildsrc.location}/.deployment"/>
    </copy>    

    <copy todir="${webcontent.location}">
      <fileset dir="${buildsrc.location}/WebContent"/>
    </copy>    

    <!-- optional task to generate the logs folder in the temp location. 
         Specific war generation tasks will log their data to this directory. -->
    <mkdir dir="${buildsrc.location}/WebContent/WEB-INF/logs"/>
    
    <!-- expand factory image into the webcontent.location -->
    <ant antfile="${webcontent.location}/projectDeploy.xml" target="expandFactoryImage" >
      <property name="runtime.image" value="${wpf.artifacts.dir}/Images/factory${version}.zip" />
    </ant>
    
    <antcall target="addPackages"/>
    
    <!-- build source files -->
    <ant antfile="${webcontent.location}/projectDeploy.xml" target="compile" />     
         
    <!-- build 286 portlet war, and give it a new name. -->
    <ant antfile="${webcontent.location}/projectDeploy.xml" target="buildPortletWar" >
      <property name="wpf.portletapi.target" value="build286StandardPortletWar"/>
      <property name="project.name" value="${project.name}"/>
    </ant>               
     
  </target>

  <target name="addPackages">    
      <fail>Your project ant script is missing a target named "addPackages". This is used to add WPF featuresets.</fail>
  </target> 

  <target name="propertiesOverride">    
      <fail>Your project ant script is missing a target named "propertiesOverride".</fail>
  </target> 
 
  <!-- clean the project -->
  <target name="clean" depends="properties">   
    <delete dir="${buildsrc.location}" />
  </target>  
</project>

Noteworthy, from top to bottom:
  • added depends="propertiesOverride" to the properties target.
  • added a call to the ant cvs task. This will retrieve any code that was added to source control.
  • removed the sample antcall to addFeatureSet and replaced it with a call to my own addPackages.
  • finally at the bottom I've added default implementations for the new targets that will cause the build to fail if they're not defined elsewhere.

Now that I've done some prep work, I can build any WPF project with a short bit of xml saved as MyAwesomeWPFProject.xml:
<project name="mainDeployment" default="build4GCPS" basedir=".">
 
    <description>Template script for building a portlet factory WAR for deployment</description>

    <import file="build.xml"/>
     
    <target name="propertiesOverride">  
        <property name="project.name" value="MyAwesomeWPFProject"/>
    </target>

    <target name="addPackages">    
        <ant antfile="${webcontent.location}/projectDeploy.xml" target="addFeatureSet" >
          <property name="pkg" value="${wpf.artifacts.dir}/Packages/Tutorials.pkg" />
        </ant>
    </target>   
    
</project>

The heavy lifting here is done with the import of build.xml, which is just like adding everything in that file to my script, but allowing me to override any targets within it. In this case I've added my own propertiesOverride where I can change values that were defined in build.properties and addPackages where I can add any featuresets that my project requires.

Building my project is now as simple as
ant -f MyAwesomeWPFProject.xml 
and if I need to build a specific version that has been labeled in CVS, I can provide that label like this
ant -f MyAwesomeWPFProject.xml -DCVSTag=MyLabel
which will override the value in build.properties.

Piece of Cake
Prior to this, I had my own homebrew build process that I am now glad to throw away. It served me well for the last three years, but this is more robust and I really like the way I can add featuresets.