Visual Studio .NET is the new standard for developing .NET applications.

Although Visual Studio .NET does a number of things very well to assist in the development process, there are certainly areas that need improvement. One of those areas is dealing with a build process that requires something a little more than just compiling source code.

In teams of developers, there is always going to be a need for a structured build process. This can be a manual process, but most likely you'll want to automate this process as much as possible. In today's ultra-fast world, continuous integration is key to determining where you are in your development project. If multiple developers check in code at various times, it is always a good thing to have an “integration check”?something to make sure all the code gels together.

The NantContrib project (http://nantcontrib.sourceforge.net) provides an assortment of additional tasks for NAnt build scripts.

The idea behind continuous integration is that when a developer checks code in, a process is triggered to perform a system build to make sure the code doesn't break the build. This action usually involves the execution of unit tests as well. If the build, along with the unit tests, succeeds, the developer has immediate feedback that the check-in didn't break the build. No one wants to be the person who broke the build!

The typical build process involves actions like these:

  • Pulling source updates from the source code repository
  • Preparing the build directories
  • Compiling the source code
  • Running the unit tests
  • Creating the setup packages
  • Deploying the output binaries
  • Notifying the team that a new build is available

If you incorporated Visual Studio .NET into a build process such as this, you would create an elaborate batch file that calls out to Visual Studio .NET's command-line (devenv.exe) along with the additional tools to handle the tasks that .NET couldn't. Another way to attack it may be to write some hooks into the .NET IDE automation model to handle some of these tasks during a build, but who has the time? The good news is that there are tools available built specifically to handle these types of build process requirements. One of the more popular build tools in the .NET community is NAnt.

NAnt

NAnt is an open-source build automation tool that works off of an XML document to execute a sequence of defined tasks that build .NET applications. NAnt is a port of the popular Ant build tool from the Java community. Ant was created to address some of the deficiencies of older shell-based build tools such as make and nmake. If you've ever written make scripts, you will appreciate NAnt's ease of use and its extensible architecture.

NAnt works well as an automated build tool. It supports compiling C#, Visual Basic .NET, C++, and J# files. It can compile these files against the .NET Framework platform [v1.x, V2.0 (Beta 1)] and Mono.

Installing NAnt

The first thing to incorporate NAnt into your build process is to get NAnt up and running on your computer. You can download the latest stable release or a recent nightly build from the project site (http://nant.sourceforge.net). The nightly builds are typically very stable, but for the purposes of this article, I will work with the latest stable release at the time of this writing, NAnt v.0.85-rc1.

Once you've downloaded the zip package, unzip the contents into a directory on your file system. To make it easier for you to execute your NAnt build scripts, place the location of NAnt's \bin directory into your system's PATH environment variable. You can test your installation by opening a command shell and typing nant ?help at the command prompt. If everything is successful, you will see a list of command-line options for the NAnt build tool.

The NAnt Build Script

Executing the build script is as simple as typing in nant at the command prompt. NAnt probes its current directory (the directory from which it was executed) for any file ending in .build and attempts to execute it. If more than one file within the directory ends in .build, NAnt throws an exception unless one of the files is named default.build. You can also inform NAnt which script you wish to execute by specifying the ?f flag:

C:> nant ?f:MyApp.build

The Visual Studio .NET project files can be incorporated into the NAnt build script through the <solution> task.

The NAnt build script is an XML document comprised of one <project> tag, and zero to many <target> tags with each <target> containing a set of tasks to execute. To make it a little easier to digest the terminology, you can think of a <target> as a method call and a task can be thought of as a command.

Each element type in a NAnt build script (the project, target, tasks, etc.) has a defined set of attributes that are associated with that particular type. Some of these attributes are required and others are optional. These attributes provide additional details about how to execute each particular type.

To better understand the build script, it is wise to get a better understanding of the main parts of any build script.

Properties

A build script can contain zero to many property definitions. A property in NAnt is analogous to a variable. You define a property by the use of a <property> tag with a name and value attribute.

&lt;property name="src.dir" value="source" /&gt;

To access a property value, you place the name of the property between the identifiers ${" and "}. For example, to access the src.dir property declared earlier, specify the following:

&lt;echo message="Source directory: ${src.dir}" /&gt;

Default property values are defined within the build script. If you need to override any property value, you can specify the ?D command-line flag and supply the new property value.

C:&gt; Nant ?D:src.dir=NotTheDefaultSourceDirectory

Properties can be set as read only by setting the Readonly attribute on the <property> tag to True.

&lt;property name="company.name" readonly="true"
          value = "Solution Partners, Inc." /&gt;

You can create property values that are global to all build scripts on your computer by defining the property definitions in NAnt's configuration file (nant.exe.config; found in the NAnt \bin directory) under the <properties> configuration section.

&lt;?xml version="1.0"?&gt;
&lt;configuration&gt;
   ...
   &lt;nant&gt;
      ...
      &lt;properties&gt;
         &lt;!-- properties defined here are 
              accessible to all build files --&gt;
         &lt;property name="company.name" 
                value = "Solution Partners, Inc."
                 readonly="true" /&gt;
      &lt;/properties&gt;
   &lt;/nant&gt;
&lt;/configuration&gt;

NAnt has a few built-in properties that can be used within your build script; a sampling of these properties can be found in Table 1.

Tasks

Tasks define a command to be executed. Tasks are XML elements with a set of defined attributes and child elements that prescribe the options for the task. For example, if you want to execute a command to delete a directory, use the <delete> task.

&lt;delete dir="bin" failonerror="false" /&gt;

The dir attribute is specific to the <delete> task and sets the option for the <delete> task to delete the directory named bin. The Failonerror attribute is common to all tasks. If the task fails, the build also fails, although there are some cases where a failed task doesn't necessarily mean the build fails. The default value is True.

If you want your build script to send an e-mail to the development team when the build completes, you could use the <mail> task.

&lt;mail from="<a href="mailto://buildmaster@foo.com">buildmaster@foo.com</a> " 
      tolist="<a href="mailto://devteam@foo.com">devteam@foo.com</a>" 
      subject="Build #${build.version} complete" 
      mailhost="<a href="http://smtp.foo.com">smtp.foo.com</a>"&gt;
   &lt;files&gt;
      &lt;include name="*.txt" /&gt;
   &lt;/files&gt;
   &lt;attachments&gt;
      &lt;include name="*.zip" /&gt;
   &lt;/attachments&gt;
&lt;/mail&gt;

Tasks define the power of NAnt. There are over 70 tasks included in the NAnt core tool. A small sampling of some the available tasks is listed in Table 2. Additional tasks are available through external libraries, such as NAntContrib (http://nantcontrib.sourceforge.net). If the available tasks don't quite get the job done, you can create custom tasks through the extensible architecture.

Although each task has its own custom attributes defined to support its execution, every task has a common set of attributes. These are listed in Table 3.

Targets

The <target> tag defines a collection of tasks to execute. They usually represent a particular step in the build process and can have dependencies on other targets. The If and Unless attributes allow for conditional target execution. When the If attribute is defined, the target only executes if the expression evaluates to True. If the Unless attribute is defined, the target is skipped when the Unless expressions evaluates to True.

Targets can be dependent on other targets. For example, if within the build script you want to clean out the output directory before the build, you might define something like this:

   &lt;target name="clean"&gt;
      &lt;delete dir="bin" failonerror="false" /&gt;
      &lt;mkdir dir="bin" /&gt;
   &lt;/target&gt;

By specifying a dependency with the Depends attribute, the target will not execute until all of its dependency targets have executed first. One of the nice features of NAnt is that it only executes a target once. So if multiple targets have a dependency on the same target, that dependent target only executes once.

   &lt;target name="clean"&gt;
      &lt;delete dir="bin" failonerror="false" /&gt;
      &lt;mkdir dir="bin" /&gt;
   &lt;/target&gt;

   &lt;target name="build" depends="clean"&gt;
   ...
   &lt;/target&gt;

Table 4 lists the attributes and the descriptions for the <Target> tag. Only the Name attribute is required.

Projects

The <project> tag is the main container for the build script. The important element of the <project> tag (other than allowing you to define a well-formed XML document) is the use of the Default attribute. When no target is specified on the command line execution of the build script, NAnt executes the target specified in the Default attribute. Table 5 lists the attributes and their descriptions for the <project> tag. None of the attributes are required.

&lt;project name="CalcApp" default="build"&gt;
   &lt;property name="src.dir" value="source" /&gt;
   &lt;property name="output.dir" value="bin" /&gt;

   &lt;target name="clean"&gt;
      &lt;delete dir="${output.dir}"
              failonerror="false" /&gt;
      &lt;mkdir dir="${output.dir}" /&gt;
   &lt;/target&gt;

   &lt;target name="build" depends="clean"&gt;
   ...
   &lt;/target&gt;

   ...

&lt;/project&gt;

NAnt and Visual Studio .NET Project Files

Visual Studio .NET does a great job in maintaining the complexities of your application definitions and configuration: the library references, the global imports and, of course, the source files. It would be a shame to have to recreate those configuration items in your build script. Thankfully the NAnt development team has created the <solution> task to allow for the use of Visual Studio .NET solution files in your build scripts.

Create your own custom NAnt task classes by creating a class that inherits from NAnt.Core.Tasks.

The <solution> task can be used to wrap a number of separate projects into the same task for compiling or use an already defined solution file and its configuration details. NAnt can use the solution file to determine project dependencies. If the projects aren't necessarily part of the same solution, NAnt is able to determine the project dependencies based on the output file name.

It's important to note that the <solution> task does not shell out to the Visual Studio .NET command line compile tool (devenv.exe) but uses the command-line compilers. With that, your build server doesn't require an installed instance of Visual Studio .NET, yet you're still able to use your project and solution definitions.

You're not limited to using a particular solution file in using the <solution> task. You are able to define a set of projects to compile by specifying the <projects> element. If you want to use a solution file that was already defined but don't necessarily want to compile every project, you can define which projects to skip by defining the <excludeprojets> element. Use the <assemblyfolders> element to provide hints to the compiler where it might find the reference DLLs. This is where you define the path(s) to your build server's output directory.

If your <solution> task contains a Web project, you are required to define a mapping between the project URL and the project's file path. It's important to remember that the <map> definition is case-sensitive. Be sure the URL attribute value matches exactly (case for case) to what's defined in your Visual Studio .NET project file. I've been bitten by this one a number of times.

Pulling It All Together

To showcase the usefulness of a NAnt build script, I have created a simple WinForms project in Visual Studio .NET, as shown in Figure 1, to add two digits together and display the result. The application consists of two source files defined within a Visual Studio .NET project (CalcApp.vbproj). Although the application itself isn't that exciting, the process of building it with NAnt is!

Figure 1: The CalcApp sample project looks like this.
Figure 1: The CalcApp sample project looks like this.

To define the build script for the CalcApp application, the first thing to do is create a text file with a .build extension (i.e., CalcApp.build). Open the build file in your favorite text editor and define the <project> tag.

&lt;?xml version="1.0" encoding="utf-8" ?&gt; 
&lt;project name="CalcApp"&gt;

&lt;/project&gt;

From there, you need to define a target responsible for building the WinForms application. Because you are using a Visual Studio .NET project to develop the application, you can use one of the core NAnt tasks, <solution>, to compile the application.

&lt;?xml version="1.0" encoding="utf-8" ?&gt; 
&lt;project name="CalcApp"&gt;

   &lt;target name="build"&gt;
      &lt;solution configuration="debug" 
                solutionfile="CalcApp.sln" /&gt;
   &lt;/target&gt;

&lt;/project&gt;

NAnt now has enough information to compile the application.

To kick off the NAnt build, open a command prompt in the location of your build script and execute the following command:

C:\Code\CalcApp&gt; nant build

The build parameter on the NAnt command line tells Nant that you wish to execute the build target. Your results should be similar to those shown in Figure 2.

Figure 2: The results of executing the NAnt build script from the command-line.
Figure 2: The results of executing the NAnt build script from the command-line.

This particular build script is sufficient but it really doesn't bring out the power of NAnt. What happens if you want to pass in the option of performing a Release build instead of a Debug build? Or what if you want to compile the application into a different output directory than what's defined in the Visual Studio .NET project file?

Listing 2 improves on the simple build script by defining two additional tasks (Release and Build) that define property settings to let NAnt know how you wish to build the application. You've defined a variable named config to store the configuration setting along with output.dir to define the location of build output. Now you have the ability to perform a debug build and a release build without having to define two different build scripts.

Building Multiple Projects

The simple CalcApp example isn't very reflective of many projects out in the real world. A number of today's applications are very complex in nature and are built by combining a number of componentized libraries. What happens when your entire application is spread across multiple solution files? With Visual Studio .NET, you would have to open each solution file and build it separately. With NAnt, you can define all of the build steps with a single build file!

With the simple CalcApp example, you have no doubt determined that the CalcLib class would be useful across multiple applications and you have probably decided to split them out into their own libraries. You then have two projects that comprise the application, CalcApp and CalcLib.

Listing 3 defines the build script for compiling both CalcApp and CalcLib, where CalcApp references?and depends upon?CalcLib. The dependency is defined by the depends attribute on <target> tag. This tells NAnt that if it is instructed to execute the CalcApp task, it is guaranteed to execute the dependency task, which is, in this case, CalcLib.

In this build script, you separated the two projects into their own respective build tasks. Also, instead of relying upon a solution file, you have instructed NAnt to compile projects based upon their project definitions only (<project> target tag). Another item of note is the reference to the assembly folders, designated by the <assemblyFolders> target tag. In this case, CalcApp references the CalcLib library by file; therefore NAnt needs to be instructed where to pull this file from. If the <assemblyFolders> is left out, NAnt attempts to locate the library from the HintPath attribute in the Visual Studio .NET project file.

To execute the CalcAppWithLib.build script, open a command prompt in the location of the build script and execute the following command:

C:\Code\CalcAppWithLib&gt; nant build

The results of this build are shown in Figure 3. From the output, you can see that the CalcLib target is executed first, followed by CalcApp, even though the configuration targets instruct NAnt to only build CalcApp. This is due to the depends attribute defined on the CalcApp target to specify that CalcLib must be executed before CalcApp.

Figure 3: Executing the NAnt build script with two targets yields these results.
Figure 3: Executing the NAnt build script with two targets yields these results.

The NAntContrib Tasks

The NAntContrib project is another open-source effort that provides additional tasks for NAnt. The tasks provided through NAntContrib are the important tasks that haven't yet made it to the NAnt core tool. Some of these valuable tasks are defined in Table 6. Most notable of these are the administrative tasks, such as mkiisdir, comregister, gac-install, and gac-uninstall, as well as several utility tasks for some of the more popular Source Control Management (SCM) tools like Visual SourceSafe, Surround SCM, StarTeam, and PVCS.

To install the NAntContrib tasks, go to the NAntContrib project site (http://nantcontrib.sourceforge.net) and download either the latest stable release or the latest nightly build. Extract the contents of the zip file onto your file system and copy the contents of the \bin directory to NAnt's \bin directory. From there, you can use any of the NAntContrib tasks in your build scripts.

The following is a list of standard practices that I recommend for using NAnt in your build process:

  • Define a separate target for each major component of your application. This allows you to build each piece independently.
  • Use properties to define a rich build script. You can provide distinctly different build scripts (i.e., Debug vs. Release) with one .build file by modifying a few of your parameters. Create a target for each modification of these parameters.
  • Define a standard set of targets across your build scripts. I use the following common targets in my build scripts:
  • Init: prepares the environment (creates directories, copies files, archives previous build directories, etc.)
  • Clean: cleans the output directory
  • Debug: sets up the property values for a debug build. These property values contain details such as the output directory (Debug vs. Release directory) and set the solution configuration to the appropriate environment.
  • Release: sets up the property values for a release build
  • All: calls all of the targets in a pre-defined order
  • Docs: generates the application's documentation using NDoc
  • Dist: distributes the compiled assemblies into the build server's distribution directory

Summary

NAnt is a great tool for automating the Visual Studio .NET build process. The open source efforts of the NAnt project provide a tool chest of tasks to create build scripts from the very simple to the exceptionally complex. It has certainly proved to a very valuable tool in my development process. With a little time invested, you will be able to institute an automated build process and free up the time to concentrate on what you want to be doing?coding.

Listing 1: An example of the <solution> task

   &lt;property name="src.dir" value="source" /&gt;
   &lt;property name="build.dir" value="bin" /&gt;
   &lt;property name="solution.config" value="debug" /&gt;
   &lt;property name="build.server.dir" value="..." /&gt;
   &lt;property name="3rd.party.lib.dir" value="..." /&gt;

   &lt;target name="build"&gt;
      &lt;solution solutionfile="${src.dir}\MyAppSolution.sln" 
          configuration="${solution.config}" 
          outputdir="${build.dir}"&gt;
      &lt;webmap&gt;
            &lt;map url="http://Localhost/MyWebApp/MyWebApp.vbproj" 
                 path="${src.dir}\web\MyWebApp.vbproj" /&gt;
         &lt;/webmap&gt;
         &lt;assemblyfolders&gt;
            &lt;include name="${build.server.dir}" /&gt;
            &lt;include name="${3rd.party.lib.dir}" /&gt;
         &lt;/assemblyfolders&gt;
      &lt;/solution&gt;   
   &lt;/target&gt;

Listing 2: Expanding the build script

&lt;?xml version="1.0" encoding="utf-8" ?&gt; 
&lt;project name="CalcApp" default="debug"&gt;
   &lt;property name="output.dir" value="bin" /&gt;
   &lt;property name="config" value="debug" /&gt;
   
   &lt;target name="release"&gt;
      &lt;property name="config" value="debug" /&gt;
      &lt;property name="output.dir" value="bin\Release" /&gt;
      &lt;call target="build" /&gt;
   &lt;/target&gt;

   &lt;target name="debug"&gt;
      &lt;property name="config" value="release" /&gt;
      &lt;property name="output.dir" value="bin\Debug" /&gt;
      &lt;call target="build" /&gt;
   &lt;/target&gt;

   &lt;target name="build"&gt;
      &lt;solution configuration="${config}" 
                solutionfile="CalcApp.sln"
                outputdir="${output.dir}" /&gt;
   &lt;/target&gt;

&lt;/project&gt;

Listing 3: Building dependent projects

&lt;?xml version="1.0" encoding="utf-8" ?&gt; 
&lt;project name="CalcApp" default="debug"&gt;
 &lt;property name="output.dir" value="bin" /&gt;
 &lt;property name="solution.config" value="debug" /&gt;
 &lt;property name="src.dir" value="${nant.project.basedir}\source" /&gt;
   
 &lt;target name="release"&gt;
  &lt;property name="solution.config" value="debug" /&gt;
  &lt;property name="output.dir" value="bin\Release" /&gt;
  &lt;call target="CalcApp" /&gt;
 &lt;/target&gt;

 &lt;target name="debug"&gt;
  &lt;property name="solution.config" value="release" /&gt;
  &lt;property name="output.dir" value="bin\Debug" /&gt;
  &lt;call target="CalcApp" /&gt;
 &lt;/target&gt;

 &lt;target name="CalcLib"&gt;
  &lt;solution configuration="${solution.config}" 
            outputdir="${output.dir}"&gt;
   &lt;projects basedir="${src.dir}\CalcLib"&gt;
    &lt;includes name="CalcLib.vbproj" /&gt;
   &lt;/projects&gt;
   &lt;assemblyfolders&gt;
    &lt;includes name="${output.dir}" /&gt;
   &lt;/assemblyfolders&gt;
  &lt;/solution&gt;
 &lt;/target&gt;

 &lt;target name="CalcApp" depends="CalcLib"&gt;
  &lt;solution configuration="${solution.config}" 
            outputdir="${output.dir}"&gt;
   &lt;projects basedir="${src.dir}\CalcApp"&gt;
    &lt;includes name="CalcApp.vbproj" /&gt;
   &lt;/projects&gt;
   &lt;assemblyfolders&gt;
    &lt;includes name="${output.dir}" /&gt;
   &lt;/assemblyfolders&gt;
  &lt;/solution&gt;
 &lt;/target&gt;
   
&lt;/project&gt;

Table 1: These are a sampling of built-in NAnt properties.

PropertyDescription
nant.project.basedirThe absolute path of the project's basedir
nant.project.buildfileThe absolute path of the buildfile
nant.settings.currentframeworkThe current target framework
nant.settings.currentframework.sdkdirectoryThe SDK directory of the current target framework

Table 2: These are just some of the tasks available in the NAnt core tool.

File System TasksDescription
<copy>Copies a file or set of files to a new file or directory
<delete>Deletes a file, a set of files or a directory
<mkdir>Creates a directory
<attrib>Modifies file attributes
Description.NET Tasks
<vbc>Compiles Visual Basic .NET files
<csc>Compiles C# files
<solution>Compiles Visual Studio .NET solutions (or sets of projects). Determines project dependencies from inter-project references
<tlbimp>Imports a type-library to a .NET assembly
<regsvcs>Installs or removes .NET Services
Misc TasksDescription
<mail>Sends an STMP message
<zip>Creates a zip file from the specified file list
<unzip>Extracts a zip package
<ndoc>Runs NDoc to create the documentation
<nunit2>Runs tests using the NUnit v2.2 framework

Table 3: These are task common attributes.

AttributesDescription
failonerrorIf set to True, stops the build if this task fails. Otherwise, it just reports.
ifAn expression that should evaluate to True for this target to execute
unlessAn expression that causes the target to be skipped when it evaluates to True
verboseDetermines whether the task will report detailed log messages

Table 4: Here's a list of <Target> attributes.

AttributesDescription
nameThe name of the target
dependsA comma-separated list of names and targets on which the target depends
ifAn expression that should evaluate to True for this target to execute
unlessAn expression that causes the target to be skipped when it evaluates to True
descriptionA short description of the target's purpose

Table 5: Here's a list of <Project> attributes.

AttributesDescription
nameThe name of the project
defaultAn expression that should evaluate to True for this target to execute
basedirThe base directory from which all paths are determined. If it isn't specified, the build script's parent directory is used.

Table 6: These are just some of the tasks available from NAntContrib.

TaskDescription
comregisterRegisters COM servers or type libraries
gac-installInstalls assemblies into the global assembly cache
gac-uninstallUninstalls assemblies from the global assembly cache
deliisdirDeletes a virtual directory from Internet Information Server (IIS)
mkiisdirCreates or modifies a virtual directory on Internet Information Server (IIS)
regasmRegisters an assembly for use from COM clients
sqlExecutes SQL statements against an OLEDB data source
vssgetUsed to retrieve items from Visual SourceSafe
vsslabelUsed to apply a label to a Visual SourceSafe item
wsdlGenerates code for XML Web service clients
xsdGenerates XML schemas or class files