CaubleStone Ink

.Net development and other geeky stuff

nAnt: Desiging your common build file (Updated)

Posted on February 2nd, 2009


In this article we will walk you through desiging a basic common build file. If done properly it’s one you can carry with you to all your projects and help you get your automated builds kicked off pretty quick.

Download the Common.xml file.

The Basics

The first thing we need to do is make sure you understand the basics of nAnt. At it’s heart it is just an xml document. As such you always need to be mindful of the strict nature of xml. Case if very important. Beyond that you have the next two items, Targets and Properties. Through these two constructs you can do just about anything. From within targets are where you will call into other tasks like, csc, vbc, or event the nant contrib and custom tasks you can build yourself.

The basic structure of a property looks like this:

<property name="MyPropName" value="MyValue"/>

As you can see it’s pretty straight forward at this level. There are other features of the property we will cover a bit later. These objects will be very important in our construction of a common build file.

Next we have Targets. This is the meat and potatoes of nAnt. Without it you don’t really have anything. The basic structure of a Target looks like this:

<target name="MyTarget">
  <!-- do something here -->
</target>

As you can see not much here either. Where it’s power comes in is from the depends attribute that allows you to chain multiple targets together into one call. There are other tasks and attributes that you can apply to your build file. If you want to see them all go to the nAnt sourceforge site.

I said it was the basics and that is just about it. The rest will depend upon what you are trying to do. Our main focus in this article will be around build C# projects, however if you need to compile to VB or some other language it would be just as easy to apply these concepts and change your tasks inside the build targets.

Basic Properties

Next we will look at setting up some of our basic properties that will be used throughout the build files. All the way from the common file to your individual build files. The first thing that you need to address though is your folder layout. How are you going to structure your project(s) on disk. This makes a difference on how you configure your project files and with a small degree your common file. The structure we will be using will be one that I’ve used for a while, it works for multiple projects to single projects. Below is a screenshot of the basic folder structure. There is nothing in the child folders as of yet.
folderlayout
The common.xml file we will be building will go into the build folder. As you add projects to your solution each project should have it’s own build file in the same folder as the project file. In addition if you have multiple distributions, say a Web site, Desktop component, and windows service as part of your deployment you can have a root distrib build file. It can be stored in the build folder or at the root of your folder structure. These extra build files can be used to control the build for each individual section of your build output.

Now let’s start by creating our first couple of properties.

<?xml version="1.0"?>
<project xmlns="http://nant.sf.net/release/0.85-rc3/nant.xsd">
  <property name="build.output.dir" value="${root.dir}\output" />
  <property name="build.dir" value="${root.dir}\build" />
  <property name="lib.dir" value="${root.dir}\build\references" />
  <property name="build.debug" value="true" overwrite="false"/>
  <property name="build.optimize" value="true"/>
  <property name="build.rebuild" value="true"/>
</project>

Now you will probably ask yourself what the ${root.dir} thing is. Well it’s essentially another property. The syntax to use a property, method, or other defined item is the syntax ${}. So in our project files which you will see later they will have a root.dir property in them. This is used to help make sure everything knows how to find what it needs. The other thing you probably noticed is the overwrite=”false” command. What this does is tells the nAnt application to only allow the build.debug command to be defined once. The reason we do this so that we can use the command line to tell the compiler if we want a debug or release build. Then we don’t need to change anything in our build files.

Now most of these property names make perfect sense. We are essentially trying to setup some variables that will hold all our path data so when it comes time to compile or run any ancillary tasks we are covered and know where everything is.

The references folder under the build folder is for 3rd party dll’s. This is where I’d put things like the log4net, nHibernate, vendor control, microsoft, etc. objects that my project needs to compile. You can also move this to a root folder called something like vendor or 3rdParty (I’ve used both before). It’s really up to you. Just change the property value to match whatever folder structure you plan on using.

Build Information

Now the next part of the common build file has a target that we will call to output some helpful information that you can use in case you have a build failure. Things like SDK paths, framework versions, etc. This can help you debug the case where say you are in a web farm and one of the machines does not have the latest .net framework and your build keeps failing. With this data you will be able to quickly see what versions are present and could adjust acordingly.

<target name="display-current-build-info">
  <echo message=""/>
  <echo message="----------------------------------------------------------" />
  <echo message=" ${framework::get-description(framework::get-target-framework())}" />
  <echo message="----------------------------------------------------------" />
  <echo message="" />
  <echo message="framework : ${framework::get-target-framework()}" />
  <echo message="description : ${framework::get-description(framework::get-target-framework())}" />
  <echo message="sdk directory : ${framework::get-sdk-directory(framework::get-target-framework())}" />
  <echo message="framework directory : ${framework::get-framework-directory(framework::get-target-framework())}" />
  <echo message="assembly directory : ${framework::get-assembly-directory(framework::get-target-framework())}" />
  <echo message="runtime engine : ${framework::get-runtime-engine(framework::get-target-framework())}" />
  <echo message="" />
  <echo message="----------------------------------------------------------" />
  <echo message="Current Build Settings"/>
  <echo message="----------------------------------------------------------" />
  <echo message="build.debug=${build.debug}"/>
  <echo message="build.rebuild=${build.rebuild}"/>
  <echo message="build.optimize=${build.optimize}"/>
  <echo message="lib.dir=${lib.dir}"/>
  <echo message="build.output.dir=${build.output.dir}" />
  <echo message="build.dir=${build.dir}"/>
  <echo message=""/>
</target>

Yes there are a lot of echo message calls. 🙂 The methods and what are available can be found on the nAnt sourceforge site. Basically what we have done is made use of some of the core items that nAnt provides to you to display a bunch of helpful information. Now you can break this down into smaller targets, say one with build specific and framework specific data, or you could ignore this altogether. It is helpful though so I’d urge you to keep the framework stuff at the least.

Build Initialization

Next we are going to create the initialization tasks. This is where we will setup basic things like making sure our output folder is clean, fileset definitions, and generally just anything you want to setup every time you need to do a build.

<target name="cleanup" if="${directory::exists(build.output.dir)}">
  <!-- do any folder clean up before we start -->
  <delete dir="${build.output.dir}" failonerror="false"/>
</target>

<target name="common.init">
  <fileset id="project.sources.cs">
    <include name="**/*.cs" />
  </fileset>
  <fileset id="project.sources.vb">
    <include name="**/*.vb" />
  </fileset>
  <mkdir dir="${build.output.dir}" if="${ not directory::exists(build.output.dir)}"/>
</target>

You will notice that we have a couple of new items introduced in this seciton. Namely the depends command and the if condition. First lets cover the if condition. Just about every task (at least that I’ve looked at) supports the if and unless conditionals. This is a great way to control your tasks. So in the case of our cleanup task what would happen if the output directory did not exist. Well we’d fail the build. We don’t want that to happen so we can add an if condition to make sure the directory exits first. If it does then we run it otherwise we just ignore this altogether.

Next the depends list. This is an important attribute to understand. Namely you need to grasp the chaining ability and order of execution for the subsequent tasks. You can add multiple targets as a dependency as long as you put a space between them. They are executed in left to right order and if you have a target that is called that has it’s own dependencies, you have to wait for them to finish too before the next one in your parent chain starts.

Next and an important one. We have setup a fileset task that allows us to define the types of items we want to be compiled. We will use the id to tell the csc task what to do. This can be very handy. We will do something similar for resources and references but they will be handled in the actuall project build files since they vary from project to project.

Now for the fileset I’ve included a sample of how you can setup your common build file to support both vb and cs. Do you need to do this? No, however if you know you need to support split languages within your build you might want to set it up for it. All you would need to do is setup multiple compile targets with the language extension as part of the target name. In the next section we will be looking at this.

Build Targets

Next we are going to setup the build targets. These are going to be the things we need to actually compile our code. Which in this case will be the csc task. As this is a common.xml file to be used with multiple projects we will setup the tasks needed to compile to DLL, EXE, and the web. The web one does not really have anything special or really all that different than the DLL task but it will allow you to add custom tasks to it that you may or may not need without impacting the other task.

<target name="common.compile.dll.cs">
  <csc
      target="library"
      debug="${build.debug}"
      optimize="${build.optimize}"
      warnaserror="${build.warnaserror}"
      output="${build.output.dir}/${project::get-name()}.dll"
      doc="${build.output.dir}/${project::get-name()}.xml"
      rebuild="${build.rebuild}"
  >
    <nowarn>
      <warning number="1591" /> <!-- No XML comment for publicly visible member -->
    </nowarn>
    <sources refid="project.sources.cs" />
    <references refid="project.references" />
    <resources refid="project.resources" />
  </csc>
</target>
<target name="common.compile.exe.cs">
  <csc
      target="exe"
      debug="${build.debug}"
      optimize="${build.optimize}"
      warnaserror="${build.warnaserror}"
      output="${build.output.dir}/${project::get-name()}.exe"
      doc="${build.output.dir}/${project::get-name()}.xml"
      rebuild="${build.rebuild}"
  >
    <nowarn>
      <warning number="1591" /> <!-- No XML comment for publicly visible member -->
    </nowarn>
    <sources refid="project.sources.cs" />
    <references refid="project.references" />
    <resources refid="project.resources" />
  </csc>
</target>
<target name="common.compile.dll.forweb.cs">
  <csc
      target="library"
      debug="${build.debug}"
      optimize="${build.optimize}"
      warnaserror="${build.warnaserror}"
      output="${build.output.dir}/${project::get-name()}.dll"
      doc="${build.output.dir}/${project::get-name()}.xml"
      rebuild="${build.rebuild}"
  >
    <nowarn>
      <warning number="1591" /> <!-- No XML comment for publicly visible member -->
    </nowarn>
    <sources refid="project.sources.cs" />
    <references refid="project.references" />
    <resources refid="project.resources" />
  </csc>
</target>

As you can see I targeted these to the .cs version. If you only want to support one language you can actually just remove the last bit off the target names. The last one is target for the web. Remember the web compiles are pretty much just DLL compiles. However if you needed to do any special copying or resource changes you could do so in this target as a base item. By no means do you need to do this if you don’t want to.

Conclusion

That just about does it for our common build file. As you can see there really is not much to it but it can really help go a long way to creating consistent builds. In the next article we will look at the project build file and how to hook these two pieces together to create a full and working build.

As a task to the reader, maybe you could look at adding tasks to execute unit testing or even building your document output based on the doc comments that are extracted during build.

** Update **
Well as sometimes happens, I fat fingered some of my cut-paste-rebuild of my common file when preparing it for this article. As I used it for the project file article I noticed a couple of things were missing. On the root element I was missing the required fields. The tag should have looked like this:

<project name="common"
	default="build"
	xmlns="http://nant.sf.net/release/0.85/nant.xsd">

Next the other thing I messed up was the dependency chain on the common.init target. It should not have had any dependencies.