The custom MSBuild task cookbook

A few years ago I wrote about building custom MSBuild tasks. I wanted to bring the topic back in the spotlight in order to prepare for a follow-up post. Since my previous post on PowerShell cmdlet development (Easy Windows PowerShell cmdlet development and debugging) has been very well-received, I decided to create a similar cookbook for those of you who're interested in building and debugging your own custom MSBuild tasks. The focus of this post is primarily on seamless development and debugger rather than on task functionality.

Step 1 - Create a Class Library project

As usual for these kind of extensions to an existing system (e.g. PowerShell, MMC, MSBuild, provider-based technologies, etc) we start by creating a class library project:

Step 2 - Import references

In order to create MSBuild tasks we need to reference a few libraries. In Solution Explorer, right click your project and select Add References. Now select Microsoft.Build.Utilities.v3.5 and Microsoft.Build.Framework from the list, both from the .NET Framework 3.5 band (note: I won't use 3.5 specific functionality in this post, so you could use the 2.0 assemblies as well, but as a general recommendation don't mix different versions):

Solution Explorer should look like this (notice I've removed a few other references I don't need but obviously this depends on your goals for the custom task):

Step 3 - Implement the task skeleton

Implementing custom MSBuild tasks isn't very difficult - all you need to do is implement the task interface ITask. However, if you try to do that you'll see there are few members to be implemented that just add boilerplate code. Instead one can derive from the abstract base class Task:

This will require the Microsoft.Build.Utilities namespace to be imported as shown above. I revealed the 'functionality' of this task in the class name in the meantime, I hope it's not too shocking :-). After importing the namespace, implement the base class:

Just one method to go, not too frightening:

Step 4 - Task parameterization

Before we dig into the Execute method we should think of adding some parameterization in order to communicate with the outside world. Although it's not really required, I bet there's little you can do without it... Parameters are simply properties on the class, more or less like parameters on cmdlets in PowerShell. Let's add a property using the prop snippet:

Press TAB twice and fill in the placeholders:

In reality you'd typically add (array) parameters of type ITaskItem because typically you'll want to reference certain files in the build system. ITaskItem is the gateway to do this but let's not go there for now. In order to make parameters required, simply add a RequiredAttribute on them. This requires importing Microsoft.Build.Framework:

Step 5 - Implementing functionality

Now it's time to provide the real functionality in the Execute method body. Let's do something simple, i.e. logging some message to the build system. In reality you'd manipulate files or so, possibly generating output (see the Output attribute) but simplicity is key in this post:

A simple two-liner: first log something in String.Format style, also specifying some importance level for the message (when running MSBuild you can control the "verbosity") and returning success (true) or failure.

Step 6 - Setting up debugging

Now comes the key take-away of this post: how to configure debugging? There are various ways of doing it, the one uglier than the other. But the following approach is pretty clean though. First, add a new item to the project (choose Add New Item in the context menu on the project node in Solution Explorer) and choose for XML file. Name it Debug.testproj:

In this file, add the following piece of XML:

Let's explain a few things:

On the Project node we refer to Debug in the DefaultTargets. We define this target a bit further in the Target node.

The UsingTask node is the most important one. Basically MSBuild loads tasks from assemblies using reflection. The TaskName attribute needs to match the class name in the assembly, in our case HelloWorldTask. To reference the assembly there are two options: AssemblyName to specify the name (e.g. MyTask, Version=1.0.0.0, PublicKeyToken=..., Culture=neutral) typically for tasks in the GAC or AssemblyFile to reference an assembly directly. We use the latter option, referring to the output of our class library project (make sure this references the the right folder where you created the project, suffixed by bin\Debug\assembly.dll).

Finally we define the Target with name Debug (as referred to in the Project node), calling our task. Calling a task consists of specifying its name as a tag and adding any of the parameters (in our case the required Name property) as attributes.

That's it:

Don't worry about the blue squirrel line, the MSBuild schema doesn't know about our custom task but that's fine. It will find it if you got the UsingTask declaration right (comparable to a using statement in C#).

Time to configure the debugger. Right-click your project and choose Properties. Go to the Debug tab and specify the following:

In the 'Start external program' textbox enter the path to your MSBuild.exe file. Make sure to use the one that matches the versions of the references assemblies in step 1 (I chose for the 3.5 assemblies, so I refer to %windir%\Microsoft.NET\Framework\v3.5\MSBuild.exe). Under 'Command line arguments' enter the path to the Debug.testproj file created above (you can find the path by marking the file in Solution Explorer and copying the Full Path property from the Properties pane).

Step 7 - Set a breakpoint and run

Time to test drive. Set a breakpoint in the code:

and hit F5. You'll see that MSBuild starts:

and our breakpoint is hit:

You can hover over the variables to get runtime information:

Press F10 to step to the next line and switch back to the MSBuild window:

Congratulations! You've successfully stepped through your first MSBuild task in the debugger.

Step 8 - Advanced debugging

Of course when more complex interactions with a complex build file are required, you'll need to do more "live debugging". In such a case multiple solutions exist:

Simply tweak the 'Command line arguments' in step 6 to point to the more complex build file and add your custom task in a similar way as described in step 6, hooking it up in the right spot.

Create a debugger assistant task that pops up a message box (MessageBox.Show) with a message "Attach debugger here" and hook it up in your to-be-tested project as a pre-build step. When launching the build (maybe even on an external machine using remote debugging) the message box will block further execution, allowing you to go to Debug, Attach to Process. You'll recognize the process to attach to by the message box's title "Attach debugger here". Set breakpoints and click OK on the message box and you're in business.

If you need to debug startup code in your task (such as a static constructor - which isn't really the best idea in most cases), the first approach will be the best since you're attaching to MSBuild right from the start.

Nevertheless, the technique outlined in this post should be good enough to cover most MSBuild custom task debugging cases.