Thursday, July 23, 2009

Writing a Custom NAnt Task (Part 4)

In this final installment of a series of posts looking into how to create a custom NAnt task, we'll dive into how to create a task that executes an external application.  While NAnt has the built in <exec> task to handling command line programs, there may come a time where the possible arguments or the command itself is just too much for such a generic task.  This was the case that I ran into while trying to integrate SchemaSpy, the Java-based database analysis and documentation tool, into my build scripts.  With the number of arguments and complexities of the program, I decided to create my own custom task.  We'll be diving into this task while exploring the differences between a basic task and an executable task.  The source code for this project will refer to the SchemaSpy Task for NAnt located out on CodePlex.

 

A Look at the Default <exec> Task.

NAnt comes bundled with a task that can be used to call external programs as a way to extend the functionality of the build script.  For some applications that take only a few arguments, this is really easy to manage.  This can be seen using a command line program for the DotNetMigrations project also located out on CodePlex

   1:  <exec program="db">
   2:      <arg value="migrate dev 2" />
   3:  </exec>

However, if you are using an item like SchemaSpy, the cleanliness of the solution begins to deteriorate fairly quickly with only a partial set of its argument.

   1:  <exec program="java">
   2:      <arg value="-jar SchemaSpy.jar -t mssql05 -host serverName -db myDb -port 1433 -sso -all -o ..\output" />
   3:  </exec>

When there is a need to integrate a program like SchemaSpy into a tool like NAnt, creating a custom task to streamline the arguments into a more user-friendly manner can greatly ease script authoring and debugging.

 

Task vs. ExternalProgramBase

In previous examples in this series, we would create our custom tasks using the NAnt.Core.Task abstract base class.  This base class gave us everything we needed to do to handle properties, children elements, and similar items.  One item that the Task base class did not handle though was running external processes though.  If you were to attempt to start a new process, an error would occur or the script would hang since the Task base class isn't thread-safe by default.  If you wanted to create the multi-threading code in order to make the process run successfully and safely, then you can definitely do such while inheriting from the Task base class; however, NAnt has already done this for you by providing the ExternalProgramBase abstract base class.  The ExternalProgramBase class inherits from Task and enhances it by providing specific methods and properties for working with external programs.  In addition, it has all of the threading logic established for you.

 

Diving Deeper into ExternalProgramBase

Since ExternalProgramBase inherits from Task, every thing we can do when we inherited from Task previously can still be done when we inherit from ExternalProgramBase.  Where the differences come into play is when we overwrite the ExecuteTask() method.  This method is called by the NAnt engine to start the task's specific function.  In the ExternalProgramBase base class implementation, this method executes the actual external application we want to start.  It does so by looking at the values of 2 properties; ExeName and ProgramArguments.  The ExeName property provides the string name (with extension) of the executable program to run.  The ProgramArguments property provides a string representing the fully list of arguments.  We can still provide our own custom code inside of the ExecuteTask() method, and once we're ready for the external program to start, we can simply set the ExeName property and call base.ExecuteTask().  The base class's ExecuteTask() method will read both properties, create the necessary threads, and execute the program.  Below is a snippet from the SchemaSpy task that illustrates this process.

   1:  /// <summary>
   2:  /// The Program Arguments listing used by the ExternalProgramBase class.
   3:  /// </summary>
   4:  public override string ProgramArguments
   5:  {
   6:      get { return BuildArgumentList(); }
   7:  }
   8:   
   9:  /// <summary>
  10:  /// Executes the task's operation for NAnt.
  11:  /// </summary>
  12:  protected override void ExecuteTask()
  13:  {
  14:      bool isValid = ValidateAttributes();
  15:   
  16:      if (!isValid)
  17:      {
  18:          Log(Level.Error, "Task Attributes are not valid.");
  19:          return;
  20:      }
  21:   
  22:      this.ExeName = "java.exe";
  23:      base.ExecuteTask();
  24:  }

Notice that in the SchemaSpy task for NAnt, I did not directly set the ProgramArguments property prior to calling base.ExecuteTask().  Instead, I had the property execute a method that would do it when the base class is ready.  This method evaluates the various properties/attributes of the task in order to create all of the arguments required by SchemaSpy to run properly.

 

The Updated Build Script

With the custom task created, we can now update the build script to use it instead of relying on the <exec> task.  Below is an example of the SchemaSpy call previously shown.

   1:  <schemaSpy
   2:      jarPath="..\SchemaSpy.jar"
   3:      dbType="mssql-jtds"
   4:      host="MyDatabaseServer"
   5:      port="1433"
   6:      dbName="MyDatabase"
   7:      schemaName="dbo"
   8:      outputDirectory="..\MyDatabaseDocumentation"
   9:      singleSignOn="true" />

 

Summary

Throughout this series of blog posts, we've covered how to make very simple custom tasks for NAnt up through the ability to create complex tasks that contain child elements and/or simplifies running external programs.  Through the steps listed in this series, hopefully all of information you need to create a custom NAnt task will be available to you.  If a scenario comes up that you need assistance with, feel free to post a comment and I'll see what I can do to assist.


kick it on DotNetKicks.com Shout it

No comments:

Post a Comment

Post a Comment