ISWIX, LLC View Christopher Painter's profile on LinkedIn profile for Christopher Painter at Stack Overflow, Q&A for professional and enthusiast programmers

Thursday, December 16, 2010

Thoughts on using C# in Build Automation

As I mentioned in my last blog post, Cary Roys has posted a blog article titled Getting Started with InstallShield Automation and C# over at InstallShield. It's a good read that raises a couple thoughts for me. I've already covered my first thought so this blog post will address my second thought.

Cary's sample starts with ( abbreviated ):


static void Main(string[] args)
{
ISWiAuto17.ISWiProject m_ISWiProj = new ISWiAuto17.ISWiProject();
}

Now I'm sure that Cary wrote this to keep things quick and simple but I wanted to use it as an opportunity to share some thoughts on build automation using C#.

In case you've never used NAnt or MSBuild ( they are very similar so everything I write about MSBuild will mostly apply to NAnt ) I'll start by saying that just as MSI shuns imperative programming in favor of declarative programming for installs,  NAnt and MSBuild do the same for build automation.   And just as MSI provides a mechanism for calling custom actions, so does MSBuild.

So, IMO, it's kind of hard to talk about using C# to write build automation without also talking about either MSBuild or NAnt. That said, you typically won't call custom EXE's using the Exec Task  for many of the same reasons why you wouldn't call an EXE from an installer except for a low risk or last resort situation.   Instead you'll write a custom MSBuild Task.

Let's look at a simple example:


using System;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Example
{
public class ExampleMSBuildTask : Task
{
public string Configuration { protected get; set; }
public override bool Execute()
{
}
}
}

This will create a DLL that exports a task called ExampleMSBuildTask. Now let's see how we actually wire it into our MSBuild targets file. ( Think Binary, CustomAction and Sequence tables in MSI )



<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="3.5" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask AssemblyFile="Example.exe" TaskName="ExampleMSBuildTask"/>
<Target Name="Build">
<ExampleMSBuildTask Configuration="Debug|Release"/>
</Target>
</Project>

In this example we have an MSBuild file that has a default target of Build which in turn calls our task. However, I've found that debugging the task isn't really straight forward. What I like to do is create a "test harness". This is just a fancy way of saying move my custom code into it's own class and consume it from both a Windows Application and an MSBuild task.

First I create a simple base class to inherit from:


using System;

namespace Example
{
class EngineBase
{
public delegate void LogHandler(string message);
public event LogHandler Logger;

virtual protected void Log(string message)
{
if (Logger != null)
Logger(message);
}

}
}

Now let's create a class that inherits from this base class:


using System;

namespace Example
{
class SampleEngine : EngineBase
{

public void Build(string Configuration)
{
Log(string.Format("Building {0}", Configuration));
}


}
}

The Build method can now easily call the logging message which will in turn call it's delegate it it exists. This basically allows the consuming class to be able to subscribe to logging messages and display it to the user in a way that's appropriate. Let's look at an example:


using System;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Example
{
public class ExampleMSBuildTask : Task
{

public string Configuration { protected get; set; }

public override bool Execute()
{
SampleEngine engine = new SampleEngine();
engine.Logger += Logger;
engine.Build( Configuration );
return true;
}

void Logger(string Message)
{
Console.WriteLine(Message);
}

}
}

The task now constructs the engine, assigns it's delegate and calls the Build member. Any messages get routed to the Console as StdOut.

Now let's look at another example:


using System;
using System.Windows.Forms;

namespace Example
{
public partial class FormTestHarness : Form
{
public FormTestHarness()
{
InitializeComponent();
}

private void buttonExecute_Click(object sender, EventArgs e)
{
var engine = new SampleEngine();
engine.Logger += Logger;
engine.Build( textBoxInput1.Text );
}

void Logger(string Message)
{
richTextBox1.Text = richTextBox1.Text + Message + "\r\n";
}

}


}

Basically we have the same code only now it's adding it to a RichTextBox on a Windows Forms. Now we have a program that we can easily run, observe and step into with a debugger without jumping through a lot of hoops. This allows you to establish your contract ( inputs and outputs ) up front, get that wired into the build automation and then do most of your development on your own box then check it all in when you are done. It also allows you to convert your logic to an EXE or NAnt task if you need to.

All in all, it's a good way to roll IMO.

No comments:

Post a Comment