Creating Custom Tasks in NAnt

Posted by Tom on 2010-07-08 21:16

If you want to get custom functionality into NAnt you have a couple of options. One is the script task, which enables you to embed C# directly into your build scripts. The other is to create a custom task. As promised, here's a brief rundown on what it takes to create your own NAnt tasks.

Your custom task will need to inherit from NAnt.Core.Task in the NAnt.Core assembly. The class must also be decorated with a TaskName attribute (this will be the tag name of the XML element you use when you call your task in the build script), and the method which actually does the work is ExecuteTask. The skeleton of your task will therefore look something like this:

using System;
using NAnt.Core;
using NAnt.Core.Attributes;
 
[TaskName("custom")]
public class CustomTask : NAnt.Core.Task
{
  protected override void ExecuteTask()
  {
      // Do stuff
  }
}

It doesn't achieve a whole lot, but that's our starting point.

Passing values from NAnt to your task . . .

Arguments are passed into your task on the NAnt side of things using attributes on the corresponding XML element. These are mapped onto properties in your C# class using the TaskAttribute decorator.

[TaskAttribute("stringproperty", Required = true)]
[StringValidator(AllowEmpty = false)]
public string StringProperty
{
    get;
    set;
}

Validation is provided for string, booleans, ints and DateTimes via the StringValidator, BooleanValidator, Int32Validator and DateTimeValidator attributes. In addition to the expected type validation, the Int32Validator will allow you to specify a range, and the StringValidator will also match regexes. Anything else you'll have to deal with yourself.

. . . And back again

The Project property of the Task base class acts as an interface to the guts of the NAnt projects. Amoung other things it gives you both read and write access to the project's properties via a string-indexed dictionary.

Project.Properties["propertyname"]

This means that should you need to get anything back into NAnt from your task, you can fire it into one of the project properties and then toy with it back in your build script. While we're on the subject, the Project object also exposes the target .NET version, base directory, project name and host of other handy features. I can't seem to find any API docs anywhere, so either crack out Reflector and go assembly spelunking or just dive around in Intellisense for a bit. It's all fairly self-explanatory.

Logging

NAnt's Task object exposes a Log property which can be used to send text back to the parent NAnt process, which then dumps it to stdout. When you send anything to the log you also supply a log level (Info, Warning, Error, Debug or Verbose) which NAnt uses to decide whether or not to display the message. So if the task has it's 'verbose' property (again, part of the base Task class) set to true then anything set with a log level of Verbose will be displayed, but hidden otherwise. NAnt uses log4net for its logging so if you've used that before then all of this should look familiar.

~fin

Finally, here's the source of the Compares Favourably NAnt task, as an example of all of the above tied together.

using System;
using System.Collections.Generic;
using NAnt.Core;
using NAnt.Core.Attributes;
using ComparesFavourably.Engine;
 
namespace ComparesFavourably.Nant
{
    [TaskName("ComparesFavourably")]
    public class Task : NAnt.Core.Task
    {
        #region Properties
 
        [TaskAttribute("settingsfile", Required = true)]
        [StringValidator(AllowEmpty = false)]
        public string SettingsFilename
        {
            get;
            set;
        }
 
        [TaskAttribute("failondifferences", Required = false)]
        public bool FailOnDifferences
        {
            get;
            set;
        }
 
        [TaskAttribute("resultproperty", Required = false)]
        [StringValidator(AllowEmpty = false)]
        public string ResultPropertyName
        {
            get;
            set;
        }
 
        private int Result
        {
            get;
            set;
        }
 
        #endregion
 
        #region Constructors
 
        public Task()
        {
            FailOnDifferences = false;
            Result = 0;
        }
 
        #endregion
 
        #region Methods
 
        protected override void ExecuteTask()
        {
            Log(Level.Verbose, "Loading comparison settings");
            ComparisonSettings settings = null;
            try
            {
                settings = ComparisonSettings.Load(SettingsFilename);
            }
            catch (System.IO.FileNotFoundException ex)
            {
                string message = "Could not find Compares Favourably settings file: " + SettingsFilename;
                Log(Level.Error, message);
                throw new BuildException(message, ex);
            }
 
            Log(Level.Verbose, "Running comparison");
            Comparison compare = new Comparison(settings);
            compare.Run();
 
            Log(Level.Verbose, "Analysing results");
            List<string> different = new List<string>();
            List<string> onlyInLeft = new List<string>();
            List<string> onlyInRight = new List<string>();
            foreach (ComparisonResultLine resultLine in compare.Result)
            {
                switch (resultLine.Result)
                {
                    case ComparisonResult.Different:
                        different.Add(String.Format("{0}.{1}", resultLine.Type, resultLine.Name));
                        break;
                    case ComparisonResult.ExistsOnlyInLeft:
                        onlyInLeft.Add(String.Format("{0}.{1}", resultLine.Type, resultLine.Name));
                        break;
                    case ComparisonResult.ExistsOnlyInRight:
                        onlyInRight.Add(String.Format("{0}.{1}", resultLine.Type, resultLine.Name));
                        break;
                }
            }
 
            if (different.Count > 0)
            {
                Log(Level.Info, "Different: " + String.Join(", ", different.ToArray()));
                Result += 1;
            }
            if (onlyInLeft.Count > 0)
            {
                Log(Level.Info, "Only In Left: " + String.Join(", ", onlyInLeft.ToArray()));
                Result += 2;
            }
            if (onlyInRight.Count > 0)
            {
                Log(Level.Info, "Only In Right: " + String.Join(", ", onlyInRight.ToArray()));
                Result += 4;
            }
 
            // Store result in the property if one is specified
            if (!String.IsNullOrEmpty(ResultPropertyName))
                Project.Properties[ResultPropertyName] = Result.ToString();
 
            // Throw a build error if that is the desired behaviour
            if (different.Count + onlyInLeft.Count + onlyInRight.Count > 0)
            {
                if (FailOnDifferences)
                    throw new BuildException("Comparison showed differences in database schemas");
            }
        }
 
        #endregion
    }
}

This has been a somewhat abridged version, but hopefully enough to get you started. As always, comments are welcome.