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

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.

December 14, 2010

InstallShield Automation Interop with .NET / C#

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.  This blog post will address the first issue.

When you add a COM reference to InstallShield, Visual Studio will generate interop libraries specific to the version that you added.  This allows you to call into the automation interface with code like:

ISWiAuto17.ISWiProject project =  new ISWiAuto17.ISWiProject();

The problem with that though is the ISWiProject class is now coupled (strongly  typed) to InstallShield 2011. If you need to reuse this code with other versions of InstallShield it simply isn't going to work.

There are several ways around this problem but none of them are pretty:

1) Use reflection for COM late binding.

//Untested Example
object projectType = Type.GetTypeFromProgID("ISWiAuto17.ISWiProject");
project = Activator.CreateInstance(projectType );


This gets ugly real fast with no type safety and a ton of line noise to invoke the members.  If a version of InstallShield doesn't support a member, you'll get a run time error.

2) Switch to C# 4.0 and use the dynamic type.   With this approach you create some initialization code that selects the correct version of InstallShield and assigns the project class to a dynamic class.  This cuts down greatly on the line noise needed to write the code but it's still not type safe and what could be a build time error will be a run time error. Also you might be working in a build automation environment that hasn't yet migrated over to .NET 4.0.

3) Write your own interfaces and then implement provider pass through wrappers for all of the automation calls you'll need to support.  This allows you to create a factory or use dependency injection.  This is a very clean approach that will be type safe but takes a lot of time to write all the tedious plumbing.

4) It's been suggested to me that the the generated runtime callable wrapper could be modified so that the type names are constant. ( Version Neutral / Neutered interop )  but it's above my skill to know how to do this.

5) Hack it!  Write your logic once, clone it to multiple class files and do a search and replace to update all the references.  Then write a factory around it to let you control which one gets used.  Any time you update your code you have to copy it over and search and replace again.   This is a horrible design but it takes a lot less time to implement then option 3 but could possibly take more time to maintain over the long run.   I've done this in the past and got lucky since the code base was pretty mature.

I'd love to see a better solution ( believe me, I've asked ), especially if it came built into InstallShield out of the box.  Anyone have any suggestions?