Colourblind

Welcome to Colourblind.

This is the personal web space of Tom Milsom. As much as possible everything is free (as in speach and as in beer).


Make text: Smaller Bigger

Creating Custom Tasks in NAnt

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

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:

   1:  using System;
   2:  using NAnt.Core;
   3:  using NAnt.Core.Attributes;
   4:   
   5:  [TaskName("custom")]
   6:  public class CustomTask : NAnt.Core.Task
   7:  {
   8:    protected override void ExecuteTask()
   9:    {
  10:        // Do stuff
  11:    }
  12:  }

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.

   1:  [TaskAttribute("stringproperty", Required = true)]
   2:  [StringValidator(AllowEmpty = false)]
   3:  public string StringProperty
   4:  {
   5:      get;
   6:      set;
   7:  }

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.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using NAnt.Core;
   4:  using NAnt.Core.Attributes;
   5:  using ComparesFavourably.Engine;
   6:   
   7:  namespace ComparesFavourably.Nant
   8:  {
   9:      [TaskName("ComparesFavourably")]
  10:      public class Task : NAnt.Core.Task
  11:      {
  12:          #region Properties
  13:   
  14:          [TaskAttribute("settingsfile", Required = true)]
  15:          [StringValidator(AllowEmpty = false)]
  16:          public string SettingsFilename
  17:          {
  18:              get;
  19:              set;
  20:          }
  21:   
  22:          [TaskAttribute("failondifferences", Required = false)]
  23:          public bool FailOnDifferences
  24:          {
  25:              get;
  26:              set;
  27:          }
  28:   
  29:          [TaskAttribute("resultproperty", Required = false)]
  30:          [StringValidator(AllowEmpty = false)]
  31:          public string ResultPropertyName
  32:          {
  33:              get;
  34:              set;
  35:          }
  36:   
  37:          private int Result
  38:          {
  39:              get;
  40:              set;
  41:          }
  42:   
  43:          #endregion
  44:   
  45:          #region Constructors
  46:   
  47:          public Task()
  48:          {
  49:              FailOnDifferences = false;
  50:              Result = 0;
  51:          }
  52:   
  53:          #endregion
  54:   
  55:          #region Methods
  56:   
  57:          protected override void ExecuteTask()
  58:          {
  59:              Log(Level.Verbose, "Loading comparison settings");
  60:              ComparisonSettings settings = null;
  61:              try
  62:              {
  63:                  settings = ComparisonSettings.Load(SettingsFilename);
  64:              }
  65:              catch (System.IO.FileNotFoundException ex)
  66:              {
  67:                  string message = "Could not find Compares Favourably settings file: " + SettingsFilename;
  68:                  Log(Level.Error, message);
  69:                  throw new BuildException(message, ex);
  70:              }
  71:   
  72:              Log(Level.Verbose, "Running comparison");
  73:              Comparison compare = new Comparison(settings);
  74:              compare.Run();
  75:   
  76:              Log(Level.Verbose, "Analysing results");
  77:              List<string> different = new List<string>();
  78:              List<string> onlyInLeft = new List<string>();
  79:              List<string> onlyInRight = new List<string>();
  80:              foreach (ComparisonResultLine resultLine in compare.Result)
  81:              {
  82:                  switch (resultLine.Result)
  83:                  {
  84:                      case ComparisonResult.Different:
  85:                          different.Add(String.Format("{0}.{1}", resultLine.Type, resultLine.Name));
  86:                          break;
  87:                      case ComparisonResult.ExistsOnlyInLeft:
  88:                          onlyInLeft.Add(String.Format("{0}.{1}", resultLine.Type, resultLine.Name));
  89:                          break;
  90:                      case ComparisonResult.ExistsOnlyInRight:
  91:                          onlyInRight.Add(String.Format("{0}.{1}", resultLine.Type, resultLine.Name));
  92:                          break;
  93:                  }
  94:              }
  95:   
  96:              if (different.Count > 0)
  97:              {
  98:                  Log(Level.Info, "Different: " + String.Join(", ", different.ToArray()));
  99:                  Result += 1;
 100:              }
 101:              if (onlyInLeft.Count > 0)
 102:              {
 103:                  Log(Level.Info, "Only In Left: " + String.Join(", ", onlyInLeft.ToArray()));
 104:                  Result += 2;
 105:              }
 106:              if (onlyInRight.Count > 0)
 107:              {
 108:                  Log(Level.Info, "Only In Right: " + String.Join(", ", onlyInRight.ToArray()));
 109:                  Result += 4;
 110:              }
 111:   
 112:              // Store result in the property if one is specified
 113:              if (!String.IsNullOrEmpty(ResultPropertyName))
 114:                  Project.Properties[ResultPropertyName] = Result.ToString();
 115:   
 116:              // Throw a build error if that is the desired behaviour
 117:              if (different.Count + onlyInLeft.Count + onlyInRight.Count > 0)
 118:              {
 119:                  if (FailOnDifferences)
 120:                      throw new BuildException("Comparison showed differences in database schemas");
 121:              }
 122:          }
 123:   
 124:          #endregion
 125:      }
 126:  }

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

Tags: dotNet, NAnt

Comments

Add Comment




Good luck with that

Please type the characters from the image above into the box below
or click here to get a new one


Submit