Writing Custom NAnt Tasks

Published 07 May 08 01:23 PM | jacobl 

Working as a Build Engineer for about 9 months, I've had the opportunity to push NAnt build scripting to the limits. Our DotImage product is made up of both managed and unmanaged code, third party libraries with .NET wrappers, code that sometimes differs from .NET 1.1 to .NET 2.0 (both x86 and x64)  and a slew of other complications. In addition to that and the multiple systems it takes to put it all together, there comes a time where a custom solution might be the best way to do things. In the world of NAnt scripts, this can come in many forms, but personally I prefer to stay away from batch files and calls to the command-line. In my mind, the most elegant option is to write a NAnt task.

For most things, NAnt tasks already exist and can be found at either their official website or at the NAntContrib sourceforge project, but it may surprise you that one of my first projects at Atalasoft was to create a NAnt task for integrating with Team Foundation Server.

We had been using Visual Source Safe so the vssget task served us well for a long time. When we migrated to TFS, we were essentially left hanging and had nothing to go on but settle for a few batch scripts that handled the task pretty well. Needless-to-say, five or so calls to tf.exe (the Team Foundation command-line utility) is not as easy to maintain as one task in a NAnt script.

In this post, I will walk you through creating a custom NAnt task with some more "advanced" features, such as adding nested elements and setting a return value. Then, in the near future, I will post my code for the tasks that I've written including tfsGet, tfsCheckOut, tfsCheckIn, removeSourceControlBindings, and others. You'll then be able to add them to your build systems and get all of the same TFS goodness that we do. I might even add them to NAntContrib as well.

Design your Task

Before I start writing a task, I like to have a good idea of what it will look like and how it will behave in a NAnt script. In the case of TFSGet, I decided to model it after the VSSGet task which is in NAntContrib.

This is what the VSSGet task looks like:

<vssget verbose="true"
        username="user"
        password="pass"
        localpath="."
        recursive="false"
        replace="true"
        writable="true"
        dbpath="\\path\to\srcsave.ini"
        path="$/path/to/project/location" />

and this is what my TFSGet task looks like:

<tfsget server="servername"
        ssl="false"
        port="8080"
        projectPath="$/path/to/project/location"
        localPath="."
        recursionType="None"
        getOptions="Force">
   <source path="*" />
</tfsget> 

Since I wrote it last year, Dave Terrell, our Build Engineer has made a few important additions such as non-default network credentials as well as the ability to return a changeset number back to the NAnt script. With these new changes, the task looks like this:

<tfsget server="servername"
        username="user"
        password="pass"
        ssl="false"
        port="8080"
        projectPath="$/path/to/project/location"
        localPath="."
        recursionType="None"
        getOptions="Force"
        changeset="changeset">
   <source path="*" />
</tfsget>

Let's Get Started 

Open Visual Studio and create a new project. NAnt tasks start their lives as Class Libraries, so that's the template that we'll choose. For this example, I'm going to create a project called TFS since it will hold all the tasks related to TFS. I might want to add more tasks in the future that aren't related to TFS so my overarching solution will be called Tasks.

After your project and solution appear in the Solution Explorer, we need to rename the Class1.cs file to TFSGet.cs. Visual Studio may ask you if  would like Visual studio to rename all references to Class1 in the project to TFSGet. Say Yes, otherwise do it manually.

Before we get started writing any code, we need to reference NAnt.Core.dll. That assembly is located in the directory structure of your NAnt install right next to NAnt.exe. Once you've done that, add the appropriate using statements to the top of your TFSGet.cs file.

Five Steps to Success

To define a NAnt task, there are five things we need to do:

  1. Define the name of the task using the TaskName class decorator.
  2. Extend NAnt.Core.Task.
  3. Create any attributes and label them with the TaskAttribute property decorator.
  4. Override the ExecuteTask method.
  5. Make sure you're output assembly ends in Tasks.dll.

The Basic Layout

Here's the skeleton for our TFSGet task:

// using ...
using NAnt.Core;
using NAnt.Core.Attributes;
// ...

namespace TFS
{
   public class TFSGet : Task
   {
      #region Task Attributes

      private string _server;
      [TaskAttribute("server", Required = true)]
      [StringValidator(AllowEmpty = false)]
      public string Server
      {
         get { return _server; }
         set { _server = value; }
      }

      // ... other attributes ...

      #endregion

 

      #region Execute

      protected override void ExecuteTask()
      {
         // The Leg Work
      }

      #endregion
   }
}

With this basic model, you can write almost any task you can think of. There are two other important concepts that this particular task employs and you should know about them and how to use them. One is returning values and the other is nested attributes. I'll explain return values shortly, but nested attributes are a bit more complicated and deserve a dedicated post. I will leave that to Dave to explain further. He's written about writing NAnt tasks with nested types as well as extending NAnt with your own types.

Using "Project"

Project gives you access to the currently running NAnt script. This is essentially the parent of your NAnt task. With it, you can log events, warnings, or errors. You also have access to properties within the project allowing you to return values from your task to the outer build script.

In the case of this example, it is incredibly useful for the build script to know the changeset of the code we got from TFS. In the script, we can then use that number to ... maybe set the version number of our assemblies for quick debugging if a customer has a question. If they give us that, we can instantly see the code that their running without having to think about it. Currently, our assembly version numbers at Atalasoft are:

6.0.<randomish number bigger than the last build>.<randomish number>

For future versions of our product we are working toward version numbers like:

x.0.<number representing the letter of the build>.<changeset number>

For example, if you called our office with a question about your 6.0.0001.38983 assemblies, we would instantly know that you are running 6.0a and we can get changeset 38983 from source control and read exactly what you've got.

Setting project properties is very simple. To set a property, you simply set the value of item indexed by the property you are setting. That was a mouthful... If you are trying to set the "changeset" property, this is what you would do:

Project.Properties["changeset"] = value;

That's it!

Logging is just as easy. To log you simply call:

Project.Log(Level.Info, "Your logged message goes here");

That message will now appear in the console if you're running your NAnt script via command line. It will also appear in your CruiseControl logs if you're using that to manage your Continuous Integration process.

Deploying a Custom Built NAnt Task

To deploy a custom NAnt task, you simply need to put it in the NAnt directory next to the nant.exe and NAnt.Core.dll that you will be running it from. The next time you call nant.exe, your newly built task will be loaded and available to be scripted against.
  
The Code

Here's the code for this task in full and here's a link to a prebuilt assembly. You will need to download and install NAnt and you may or may not have to download and install Visual Studio TeamFoundation Server SDK for access to the Microsoft.TeamFoundation namespace items.

Check back in the future for more NAnt tasks and be sure to read Dave's articles on using nested types as well as creating your own

Filed under: , , , ,

Comments

# DotNetKicks.com said on May 8, 2008 1:02 AM:

You've been kicked (a good thing) - Trackback from DotNetKicks.com

# Jake Opines said on May 14, 2008 11:37 AM:

Before I get started, I want to say thanks to Mike Carbenay and his article on CodeProject about removing

# Jake Opines said on May 21, 2008 8:04 AM:

Before I get started, I want to say thanks to Mike Carbenay and his article on CodeProject about removing

# Sean Gilbert said on December 16, 2008 4:04 PM:

Custom NAnt Task for Intellisense in VS.NET

# Jake Opines said on December 24, 2008 10:52 AM:

Here are a few custom NAnt tasks we use in-house for our TFS needs: tfscheckout, tfscheckin, tfsmerge,

# raghusrikanth said on March 19, 2009 2:28 AM:

hi Jake,

I have a requirement like i need to create a custom nant task like propert task which can hold multiple values means it should resemble the array. Can u please help me some thing on this.

Thanks

Anonymous comments are disabled