Geekdojo

Tech people talking tech things
Welcome to Geekdojo Sign in | Join | Help
in Search

Richard Case

Creating a custom NAnt task

I've been using NAnt quite a bit lately for automated builds on projects that I have been working on. If like me you require functionality that isn't already within NAnt (or NAntContrib) then you need not worry as it's really easy to write your own custom NAnt tasks to accomplish pretty much anything you could want.

This post will take you through the creation of a task that I recently wrote called xmllist . I created the task as I wanted to be able to read a list of port names from a BizTalk binding file (which is an xml document) and then loop round this list starting/stopping the ports:

    <xmllist property="ports.list" doc="${binding.file}" selection="//SendPort/@Name" />    
    <foreach item="String" delim="," in="${ports.list}" property="port.name">    
        <exec program="cscript.exe" failonerror="false" commandline="/nologo ${scripts.path}\StartSendPort.vbs ${port.name}"/>   
    </foreach>


The steps required to create the task are:

1. Create a new Class Library project
2. Change the project properties so that the name of the output assembly ends with Tasks.dll (i.e. Solidsoft.Build.Tasks.dll). If you don't do this NAnt will not find your tasks.
3. Add a reference to NAnt.Core.dll found in the NAnt bin directory. This assembly contains the base classes and attributes that are required to build a NAnt task.
4. Rename the class file to XmlListTask.cs
5. At the top of the class file add the following using statements:

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

6. We need to inherit our class from one of the NAnt base task classes. There are a number that we can use depending on our requirements - for our task we will use Task (I will be covering using TaskContainer in a future posting).
7. Next we need to add the TaskName attribute to our class definition. The purpose of this is to specify the tag name used for our task in a NAnt script. In our case we will use xmllist:

    [TaskName ("xmllist")]
    public class XmlListTask : Task
    {
  

8. Each NAnt task can have a number of properties. These are represented in the NAnt build file as attributes of the task element. We will have the following properties:

property Used to specify the name of the property that the list of values should
be stored in
doc The document that should be used
selection The XPath expressions that is used to select the values for the list.

Adding a property to your task is easy. For example, for the 'doc' property we would define a string property within our class. Then we use the TaskAttribute attribute to specify the name of the attribute that will be used in the NAnt build file to map back to the property in the class that we just created:

    [TaskAttribute ("doc", Required = true)]
    [StringValidator (AllowEmpty = false)]
    public string XmlDocument
    {
      get
      {
        return m_xmldoc;
      }
      set
      {
        m_xmldoc = value;
      }
    }
  
Properties can be optional or mandatory. To mark a property as required (the user must use the attribute in the build file) you can add Required = true to the attribute - as we have done above. By default properties are optional.

You will also see that I have used the StringValidator attribute. This allows us to catch the situation where the attribute has been defined without a value (e.g. <xmllist doc="" />). NAnt will see that no value has been supplied and will stop the build with an error.

Add code to create the property and selection properties - both of which are strings.

9. Now we get to the interesting bit - the part that actually performs the task. When you run your NAnt build file and it encounters your task it will first read the attributes of the task and populate the relevant properties in your class. Then it will execute your task by calling ExecuteTask. You will need to override the ExecuteTask method of the base Task class. Within this method you place the code to do what ever it is that your task does. In our case create a comma delimited list of values from an xml document. The code I used is below:

 protected override void ExecuteTask()
    {
      //Check the file exists
      string docPath = Project.ExpandProperties(m_xmldoc, Location);
      if (File.Exists(docPath) == false)
      {
        throw new BuildException("The xml document specified does not exist");
      }
      // Load the document and run the selection
      XmlDocument doc = new XmlDocument();
      doc.Load(docPath);
      XmlNodeList list = doc.SelectNodes(m_selection);
      Project.Log(Level.Info, "Found " + list.Count.ToString() + " nodes");
      System builder = new System.Text.StringBuilder();
      foreach (XmlNode node in list)
      {
        builder.Append(node.InnerText);
        builder.Append(",");
      }
      if (builder.Length > 0)
      {
        builder.Length -= 1;
      }
      if (Project.Properties.Contains(m_property) == true)
      {
        Project.Properties[m_property] = builder.ToString();
      }
      else
      {
        Project.Properties.Add(m_property, builder.ToString());
      }
    }
 


I won't take you through every line as it should be self explanatory. The main thing to note is the use of the Project class throughout the method. The Project class is used to represent the project that is being run and it's what we use to access most project related stuff (i.e. project properties).

The ExpandProperties method is used to ensure that any properties (not sure what NAnt properties are then go here) that are included as expanded to their actual value. For example, ${nant.project.basedir} will be replaced with the path to the directory where the NAnt script is located.

The Log method is used to output messages to the log file / screen - useful for informing the user what is happening and for debugging purposes. You can specify a log level - for example you can have messages that are only shown if are running NAnt in verbose mode.

10. Build the project and copy the assembly to the NAnt bin directory. If you plan to debug it then additionally copy across the pdb file. That's it.


I use 2 methods to debug my custom tasks:

1. Copy the assembly (and pdb) file to the NAnt bin directory. Open your solution in Visual Studio that contains the source for your task. Place your breakpoints. Go to the project properties and open the Debugging page. Change the Debug Mode to Program and the Start Application to the path to the NAnt executable (e.g. C:\Program Files\NAnt\bin\NAnt.exe). Then set the working directory and/or command line arguments so that NAnt will pick up your build file. Click run and away you go.
2. Place System.Diagnostics.Debbugger.Break(); in your code before the line you want to break on. Re-compile the project and copy the assembly (and pdb) to the NAnt bin directory. When you run your NAnt script you should get a popup box asking you to choose a debugger.

Published Thursday, January 06, 2005 1:41 AM by rcase

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

rcase said:

great! :)

Thank you!
January 21, 2005 11:06 PM
 

rcase said:

January 22, 2005 11:13 AM
 

rcase said:

May 7, 2005 5:53 PM
 

Christophe's Blog said:

July 27, 2005 10:09 AM
 

Christophe's Blog said:

July 27, 2005 10:24 AM
 

Christophe's Blog said:

July 27, 2005 10:29 AM
 

bgeek said:

One of the only lacking things in nant is the ability to count nodes which match an xpath expression. which it turn would allow you to rotate through the entire list of matching nodes (using a simple emulated for loop)...
September 20, 2005 5:52 AM
 

Serge van den Oever [Macaw] said:

December 1, 2006 6:27 AM
 

Bgeek.net said:

Nant XML Node Counting
August 21, 2007 2:17 AM

What do you think?

(required) 
(optional)
(required) 

This Blog

Post Calendar

<January 2005>
SuMoTuWeThFrSa
2627282930311
2345678
9101112131415
16171819202122
23242526272829
303112345

Post Categories

Syndication

Powered by Community Server, by Telligent Systems