Creating Custom NAnt Tasks

This article is the fourth in a series of articles on the build tool called NAnt. It describes how to use C# to create custom NAnt tasks. The information provided here builds on the previous articles, which can be found here, here and here. In particular, see the last two articles. The source code for this article, and for the last two articles, is available here.

The techniques provided for extending NAnt represent an excellent example of modern code design. The whole concept of plug and play, extensible, architectures is very much at the apex of current application design theory, and the folks who brought us NAnt demonstrate exactly how this kind of architecture should behave. As you will see while reading this article, the concepts involved are easy to understand, and easy to use. This is an excellent example of the kind of intuitive, cutting edge program design that lies behind the best modern software.

NAnt Tasks

Since this article focuses on building custom NAnt tasks, it seems appropriate to spend a few sentences defining our terms. NAnt is based on XML scripts. Each script contains XML code built around a series of tasks. A task plays a similar role in NAnt to a method or class in C#. It is a chunk of executable code.

Most people who use NAnt rely on the built in tasks that come with NAnt. For instance, the echo task writes a string to the console.

<project name="EchoProject" default="talk">

	<target name="talk">
		<echo message="This is a simple echo task."/>
	</target>

</project>

If you save this file as echo.build, and then run it with this command nant -buildfile:echo.build, the output would look like this:

[D:\temp\bar]nant -buildfile:echo.build
NAnt 0.85 (Build 0.85.1932.0; rc3; 4/16/2005)
Copyright (C) 2001-2005 Gerry Shaw
http://nant.sourceforge.net

Buildfile: file:///D:/temp/bar/echo.build
Target framework: Microsoft .NET Framework 1.1
Target(s) specified: talk


talk:

     [echo] This is a simple echo task.

BUILD SUCCEEDED

Total time: 0 seconds. 

Note the highlighted line near the bottom of this listing where our text is echoed to the console. Besides the echo task, there are many other built in tasks, such as the csc task, used to build CSharp projects, or the exec task, used to run a system command.

Sometimes the built in tasks do not provide all the functionality you might need. In that case, you can create a custom task of your own. The purpose of this article is to show you the simple steps necessary to create your own custom task. After you have created the task, it will be saved to a DLL, where it can be easily reused in other projects.

Putting the Config Script in a Task

In the previous article in this series, you saw how to write C# script that can be embedded directly in a NAnt build file. In that article, the goal was to make identical changes to a series of .NET app.config or web.config files. This article will take that C# script from the previous article, embed it in a task called ConfigTask. In particular, the ConfigTask will be stored in a .NET assembly. Once you have built the assembly, you will be able to reuse it in multiple projects.

The relevant code from the last article is shown in Listings 0, 1 and 2. In particular, the code highlighted in dark blue in Listing 1 is the part we need to move into our .NET assembly. Listing two shows a second build file that this file relies upon. These files also assume that there are three files called app.config in subdirectories of the current directory. These config files are the files that are to be updated by this code. For clarification, please read the previous articles and download the source code.

Listing 0: The app.config file which is modified by the code shown in Listings 1 and 2.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="AppName" value="TradeMaster" />
    <add key="DatabaseName" value="Velocity" />
    <add key="FtpAddress" value="localhost" />
  </appSettings>
</configuration>

Listing 1: Here is the build file, called default.build, from the previous article. It contains, highlighted in blue, the code that we want to reuse.

<project name="ConfigChanger" default="fancyCode">
    
	<target name="talk">
		<echo message="You ran default.build"/>
	</target>

	<target name="setValues">
		<property name="fileName" value="config01\app.config"/>
		<property name="attributeName" value="DatabaseName"/>
		<property name="valueName" value="Zen"/>
	</target>  

	<target name="configWrite" depends="talk">

		<nant buildfile="nant.build" target="configWrite">
			<properties>
				<property name="fileName" value="${fileName}"/>
				<property name="attributeName" value="${attributeName}"/>
				<property name="valueName" value="${valueName}"/>
			</properties>
		</nant>

	</target>


	<target name="fancyCode">

		<script language="C#">
			<code>
			<![CDATA[
			public static void NewProp(
				Project project, string fileName, 
				string attributeName, string valueName)
			{
				string fileNameProperty = "fileName";
				string attributeNameProperty = "attributeName";
				string valueNameProperty = "valueName";

				project.Properties.Remove(fileNameProperty);
				project.Properties.Add(fileNameProperty, fileName);

				project.Properties.Remove(attributeNameProperty);
				project.Properties.Add(attributeNameProperty, attributeName);

				project.Properties.Remove(valueNameProperty);
				project.Properties.Add(valueNameProperty, valueName);
			}

			public static void DoFileNames(Project project, 
				string attributeName, string valueName)
			{
				String[] fileNames = {"config01\\App.config", 
					"config02\\App.config", 
					"config03\\App.config"};

				for (int i = 0; i < 3; i++) 
				{
					NewProp(project, fileNames[i], 
						attributeName, valueName);
					project.Execute("configWrite");
				}
					
			}

			public static void ScriptMain(Project project) 
			{   
				project.Execute("setValues");
				DoFileNames(project, "DatabaseName", "Zen");
				DoFileNames(project, "AppName", "TradeMaster");
				DoFileNames(project, "FtpAddress", "localhost");
			}
			]]>
			</code>
		</script>

	</target>

</project>

Listing 2: This file, called nant.build, is a dependency of the default.build file shown in Listing 1.

<project name="testName" default="talk">

	<target name="talk">
		<echo message="You ran NAnt.build"/>
	</target>

   	<target name="configWrite" depends="talk">

		<xmlpoke
			file="${fileName}"
			xpath="/configuration/appSettings/add[@key='${attributeName}']/@value"
			value='${valueName}'>
		</xmlpoke>

</target>

</project>

Since these three files were exhaustively discussed in the previous article, I will say no more about them here. Instead, I will show you, in Listing 3, the complete code for the .NET assembly that we are going to create.

Listing 3: Here is the first version of the code for the .NET assembly which will contain our new task. You should save this code to disk as ConfigTask.cs.

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


namespace NAnt.Examples.Tasks 
{
	[TaskName("configtask")]
	public class ConfigTask : Task 
	{   
		public void NewProp(Project project, 
			string fileName,
			string attributeName,
			string valueName)
		{
			const string fileNameProperty = "fileName";
			const string attributeNameProperty = "attributeName";
			const string valueNameProperty = "valueName";

			project.Properties.Remove(fileNameProperty);
			project.Properties.Add(fileNameProperty, fileName);

			project.Properties.Remove(attributeNameProperty);
			project.Properties.Add(attributeNameProperty, attributeName);

			project.Properties.Remove(valueNameProperty);
			project.Properties.Add(valueNameProperty, valueName);
		}

		public void DoFileNames(Project project, string attributeName, string valueName)
		{
			String[] fileNames = {"config01\\App.config", 
				"config02\\App.config", 
				"config03\\App.config"};

			for (int i = 0; i < 3; i++) 
			{
				NewProp(project, fileNames[i], attributeName, valueName);
				project.Execute("configWrite");
			}

		}

		public void RunConfig(Project project) 
		{   
			project.Execute("setValues");
			DoFileNames(project, "DatabaseName", "Zendo");
			DoFileNames(project, "AppName", "TradeMaster");
			DoFileNames(project, "FtpAddress", "localhost");
		}

		// Override the ExecuteTask method.
		protected override void ExecuteTask() 
		{
			RunConfig(Project);
		}
	}
}

Here, with all extraneous code removed, is the core structure around which this code is build:

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


namespace NAnt.Examples.Tasks 
{
	[TaskName("configtask")]
	public class ConfigTask : Task 
	{   
		// Override the ExecuteTask method.
		protected override void ExecuteTask() 
		{
			RunConfig(Project);
		}
	}
}

As you can see, we use the TaskName attribute at the top of our class, and descend the class itself from the NAnt class called Task. We then have to override a single method called ExecuteTask. From inside that method, we call our custom method named RunConfig. In the previous article in this series, the RunConfig method was called ScriptMain. This new version of our code is identical to what you saw in the previous article, except for this one name change, and the deletion of the static declarations for each of the methods.

Building and Calling the Task

The build file which will compile our assembly , load it into memory, and execute our task is shown in Listing 4.

Listing 4: The file config.build compiles our assembly, loads it into memory, and executes our task. Type nant -buildfile:config.build to run this file.

<?xml version="1.0"?>
<project name="NAnt" default="run">

	
	<property name="libName" value = "bin\ConfigTask.dll"/>

	<target name="talk">
		<echo message="You ran default.build"/>
	</target>

	<target name="setValues">
		<property name="fileName" value="config01\app.config"/>
		<property name="attributeName" value="DatabaseName"/>
		<property name="valueName" value="Zen"/>
	</target>  

	<target name="configWrite" depends="talk">

		<nant buildfile="nant.build" target="configWrite">
			<properties>
				<property name="fileName" value="${fileName}"/>
				<property name="attributeName" value="${attributeName}"/>
				<property name="valueName" value="${valueName}"/>
			</properties>
		</nant>

	</target>

    <!-- Compile the test task and add it then use it. -->
    <target name="build">
        <mkdir dir="bin" />
        <csc target="library" output="${libName}">
            <sources>
                <include name="ConfigTask.cs"/>
            </sources>
            <references basedir="${nant::get-base-directory()}">
                <include name="NAnt.Core.dll"/>
            </references>
        </csc>
    </target>

    <target name="run" depends="build">
        <!-- Dynamically load the tasks in the Task assembly. -->
        <loadtasks assembly="${libName}" />

        <!-- Call our new task -->
        <configtask/>
    </target>

    <target name="clean">
        <!-- Delete the build output. -->
        <property name="configFull" value="${directory::get-current-directory()}\${libName}"/>
		<echo message="Deleting ${configFull}"/>
        <delete file="${configFull}" if="${file::exists('bin\ConfigTask.dll')}" />
    </target>
</project>

The build target found in this code is straightforward NAnt code for creating an assembly.

	<property name="libName" value = "bin\ConfigTask.dll"/>

 <!-- Compile the test task and add it then use it. -->
    <target name="build">
        <mkdir dir="bin" />
        <csc target="library" output="${libName}">
            <sources>
                <include name="ConfigTask.cs"/>
            </sources>
            <references basedir="${nant::get-base-directory()}">
                <include name="NAnt.Core.dll"/>
            </references>
        </csc>
    </target>

The built in csc task is used to perform the compilation, and the library is saved to the relative path bin\ConfigTask. If the bin directory does not exist, it will be created automatically using the built in mkdir task. Note that the code references NAnt.Core.dll. This library contains the Task class referenced by our C# code. Any developer who wants to create a large number of sophisticated tasks should become familiar with this DLL.

Once the library is built, it needs to be loaded into memory. The following code performs that task:

<property name="libName" value = "bin\ConfigTask.dll"/>

<target name="run" depends="build">
	<!-- Dynamically load the tasks in the Task assembly. -->
	<loadtasks assembly="${libName}" />

	<!-- Call our new task, converts the message attribute to all caps and displays it. -->
	<configtask/>
</target>

As you can see, there is a specific task, called loadtasks, which is designed to load an assembly into memory. Once it is loaded, we are free to call our task:

 <configtask/>

Assuming that nant.build is in place, and you have three app.config files like the one shown in Listing 0 are in the appropriate place, then you should find changes made to the config files after running the task.

Passing Information into Our Task

As it exists now, our task has a considerable amount of hard coded information in it. For instance, the names of the attributes in the config files that we want to change are hard coded, as well as the names of the config files themselves. There are several ways to remedy this situation, but perhaps the simplest would be to pass in a file name, and then have the C# code in our task read data from that file.The file, would, of course, contain data such as the paths to our config files, as well as the attribute and value pairs that we want to modify.

The code shown in Listing 5 extends our assembly to include an attribute for our task called fileName. You can use this attribute to pass in the file name which will contain configurable data such as the names of the config files and the attributes inside them that you want to change. The code shown here demonstrates how to pass the file name into the task, but the rest of the work will be left either to a future article, or as an exercise for the reader.

Listing 5: Code for passing in data to our assembly. In particular, a new attribute called fileName has been added to this code.

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

namespace NAnt.Examples.Tasks 
{
	[TaskName("configtask")]
		public class ConfigTask : Task 
	{
		private string _fileName;

		[TaskAttribute("fileName", Required=true)]
		public string FileName 
		{
			get { return _fileName; }
			set { _fileName = value; }
		}

		public static void NewProp(Project project, 
								   string fileName,
								   string attributeName,
								   string valueName)
		{
			const string fileNameProperty = "fileName";
			const string attributeNameProperty = "attributeName";
			const string valueNameProperty = "valueName";

			project.Properties.Remove(fileNameProperty);
			project.Properties.Add(fileNameProperty, fileName);

			project.Properties.Remove(attributeNameProperty);
			project.Properties.Add(attributeNameProperty, attributeName);

			project.Properties.Remove(valueNameProperty);
			project.Properties.Add(valueNameProperty, valueName);
		}

		public static void DoFileNames(Project project, string attributeName, string valueName)
		{
			String[] fileNames = {"config01\\App.config", 
				"config02\\App.config", 
				"config03\\App.config"};

			for (int i = 0; i < 3; i++) 
			{
				NewProp(project, fileNames[i], attributeName, valueName);
				project.Execute("configWrite");
			}

		}

		public static void RunConfig(Project project) 
		{   
			project.Execute("setValues");
			DoFileNames(project, "DatabaseName", "Zendo");
			DoFileNames(project, "AppName", "TradeMaster");
			DoFileNames(project, "FtpAddress", "localhost");
		}

		protected override void ExecuteTask() 
		{   
			RunConfig(Project);
			Log(Level.Info, "You passed in: " + _fileName);
		}
        
	}
}

The code that we care about in this new version of our assembly is really very simple:

private string _fileName;

[TaskAttribute("fileName", Required=true)]
public string FileName 
{
	get { return _fileName; }
	set { _fileName = value; }
}

A private variable named _fileName is declared at the top of the file. Then the attribute for our task is declared, and finally we create a simple property called FileName which implements the new attribute. Please note that we have set the Required flag to true, which means that the fileName attribute must be included when you call this task.

Making use of this attribute is very simple. As you recall, in Listing 4 we called our task from a build file with this code:

<configtask/>

Now we add our new TaskAttribute to the XML which calls out task:

<configtask fileName="MyFile.txt"/>

At the very end of our new version of the assembly, shown in Listing 5, we print out the value of our attribute using the Log method of Task::Project. In actual working code, the task should open the file and read data from it. But for now, we will just print out the file name:

protected override void ExecuteTask() 
{   
	RunConfig(Project);
	Log(Level.Info, "You passed in: " + _fileName);
}

Summary

This short article has provided the basic information you need to create your own NAnt tasks. You have seen that tasks are stored into assemblies, and you have seen the code to create a simple assembly that contains a task. A build file was shown that can build the assembly, load it into memory, and then call it. The article ended with a description of how to pass information into a task.

If understand the code demonstrated in this and previous articles, then you should be master of enough information to really put NAnt to work. The NAnt project is beautiful designed, and the mechanisms it provides for either writing C# script or creating your own tasks are both elegant and highly usable.

Five or six years ago, I remember sitting in the LA airport talking to my friend Mark Miller. At the time, he was interested in creating a plug and play, extensible architecture for his CodeRush product. He was very fired up about the concept, and we talked about the idea for over an hour. All these years later, projects like Eclipse and NAnt have brought that vision to fruition. A lot of smart programmers have been thinking about these ideas for years, and it is great to see them as beautifully executed as they are in the NAnt project.

No comments yet

Leave a Reply

You must be logged in to post a comment.