This is post 6 in a series devoted to extending CruiseControl.NET to support multiple build configurations for a single project. In this post, I will discuss a new component: the server-side plugin.
Up until now, we’ve limited our discussion to dashboard plugins. So far we’ve implemented a simple page that will allow a user to modify NAnt property values for a project. However, we are missing two very important pieces: the acquisition of the properties, and the committing of those properties once they have changed.
Unfortunately, we’re limited in what we can do with a dashboard plugin. The dashboard can be considered nothing more than a front-end for the actual ccnet service. It lets the user kick off builds, and inspect the status of current or past builds. It has some rather sophisticated reporting services, but provides little in the way of actually manipulating the ccnet service itself.
That entire paragraph is the long-winded way of saying that the dashboard api does not provide us with a means to really *do* anything with a given project. That’s why we need a server side plugin.
NAnt script properties
Now that we’ve decided that we would like our plugin to modify the properties for a nant script, we need to decide how to actually accomplish this work.
Those that are familiar with the existing ccnet/nant integration will recall that nant task has a property called BuildArgs. This property allows the configuration author to augment how nant executes a script by passing nant command line arguments. We will use this property and a bit of inheritance.
Custom Task
I’ve created a new class called ParameterizedNAntTask which implements ccnet’s ITask interface. This task will be substitute your existing nant task references in your ccnet configuration. This task will internally delegate to the existing NAnt task, so we benefit from the existing nant integration behavior. Additionally, the task will expose an array of PropertyItem objects(this class was introduced in the last post). When this task is executed, the properties will be passed to the inner nant task via the BuildArgs property, so that the properties are injected into the nant script at runtime.
The full code listing for this class follows:
/// <summary>
/// This object is the heart of the server side plugin. This object implements the core ITask interface, and
/// wraps/delegates CCnet's built-in nant task.
///
/// This task allows the delegated nant task to be paramterized.
/// </summary>
[ReflectorType( "parameterizedNant" )]
public class ParameterizedNAntTask : ITask
{
/// <summary>
/// The inner nant task
/// </summary>
private NAntTask _innerTask;
/// <summary>
/// The properties for the build
/// </summary>
private PropertyItem[] _properties;
/// <summary>
/// Gets/Sets the inner nant task
/// </summary>
[ReflectorProperty( "nant" , Required = true )]
public NAntTask InnerTask
{
get
{
return _innerTask;
}
set
{
_innerTask = value;
}
} //END InnerTask property
/// <summary>
/// Gets/Sets the array of property objects that will be passed to the inner nant task
/// </summary>
[ReflectorArray( "properties" )]
public PropertyItem[] Properties
{
get
{
return _properties;
}
set
{
_properties = value;
}
} //END Properties property
#region ITask Members
/// <summary>
/// Delegates to the inner nant task
/// </summary>
public void Run( IIntegrationResult result )
{
// set the nant tasks's build args; the existing object's build args will be replaced
string argText = GetBuildArgs();
_innerTask.BuildArgs = argText;
_innerTask.Run( result );
} //END Run method
#endregion
/// <summary>
/// Converts the defined property items into name/value pairs that will be
/// passed to nant via the command line(see the -D: nant switch)
/// </summary>
/// <returns></returns>
private string GetBuildArgs()
{
StringBuilder sb = new StringBuilder();
foreach( PropertyItem item in Properties )
{
string arg = string.Format( " -D:{0}={1}" , item.Name , item.Value );
sb.Append( arg );
} //END property loop
return sb.ToString();
} //END GetBuildArgs method
} //END ParameterizedNAntTask class
The first thing of note is the ReflectorType attribute; I’ve decorated the class with this attribute, naming it “parameterizedNant”. Remember, this value is how the task will be referred from the configuration file.
The task has only two members: a private inner NAntTask object, and an array of PropertyItem objects. Each of these members are accessed via public properties.
The NAntTask object reference is wrapped via the InnerTask public property(read/write). This property is decorated with the ReflectorProperty attribute, named “nant”, and is required.
The PropertyItem array is accessed via the Properties public property. This property is decorated via the ReflectorArray attribute, which is likewise named “properties”.
** This custom task does not instantiate either of these members. We are relying on ccnet to inject these object references via the property setters.
That leaves two methods: ITask.Run, and GetBuildArgs.
GetBuildArgs
The GetBuildArgs method returns a simple string that will contain all of the property declarations. This value will be provided to the inner nant task’s BuildArgs property so the property values will be injected into the script at runtime.
It’s a fairly simple method: it simply loops through each PropertyItem in the Properties array, appending the property declaration in the form of ” -D:[name]=[value]“. Notice that each property value is not quote-escaped; so the property values within the configuration itself should be quote-escaped as necessary.
ITask.Run
The ITask interface defines a single method: void Run( IIntegrationResult ). Fortunately for us, we don’t need to figure out how to work with the IIntegrationResult interface because we’re delegating to an existing ITask implementation that already knows how to do so. Our implementation of this method simply calls the GetBuildArgs method to get an argument list of properties, and sets the inner nant task’s BuildArgs property to this value. Then it delegates to the inner task’s Run method. Pretty simple.
So what does this look like in a configuration file?
You’ve seen the code for the server side plugin. I hope it makes sense. But to put it into context, here is a bit of sample xml that would be used in the configuration file:
<cruisecontrol>
<project>
<tasks>
<parameterizedNant>
<nant>
<buildArgs />
<buildFile>main.build</buildFile>
<buildTimeoutSeconds>1320</buildTimeoutSeconds>
<baseDirectory />
<executable>nant.exe</executable>
<logger>NAnt.Core.XmlLogger</logger>
<nologo>True</nologo>
<targetList />
</nant>
<properties>
<property>
<name>build.release</name>
<value>false</value>
</property>
<property>
<name>prop2</name>
<value />
</property>
<property>
<name>prop3</name>
<value />
</property>
</properties>
</parameterizedNant>
</tasks>
</project>
</cruisecontrol>
Notice that the parameterizedNant declaration is inserted before the normal nant declaration. In this case, you would define your nant setup normally. After the nant declaration is the element, which contains a series of elements.
So from this sample xml, and from the code listing, one can infer that when ccnet kicks off this project, it will execute nant and pass it the “build.release”, “prop2″, and “prop3″ property values via the command-line.
What’s next?
We have a dashboard plugin. We now have a server-side plugin. What’s next? Now we need to find a way for the dashboard plugin to augment the configuration for a project and set the embedded property values for the parameterizedNant task. That is the object of the next post.