Debugging MSBuild script with Visual Studio

Debugging MSBuild script with Visual Studio

Rate This
  • Comments 24

Back when we started 4.0 development, I polled readers of the MSBuild blog to find out what features were most important to them. Debugging was #1 which was very surprising to us. Thinking about it more, it makes sense. In our team we've become so proficient ourselves at reading the XML and making sense of logs that it's easy to forget how difficult it is – especially for someone new. John Robbins, debugging guru, also requested a Visual-Studio-integrated debugger.

Fast forward to the 4.0 release earlier this year, and we addressed 7 out of 16 of the requests by my count. We had to balance the requests with what Visual Studio itself needed from MSBuild. There were two major requirements it had on MSBuild: to enable VC++ to move onto MSBuild (#5 request), and to help enable more powerful and fine grained multi-targeting.

It turned out that these two in turn required many other features, most of which were happily also popular requests on that blog poll. We added the ability to define a task with inline code (#7 – see powershell example) a new, comprehensive object model (#14; in three parts, one, two, three), improved performance and scalability in many cases (#8 -- and here), property and item functions (#9 – albeit not currently extensible) , and accurate automatic dependency checking by performing file system interception (#11), plus some small syntax additions (label, import group, import by wildcard) and a more configurable build engine (eg see here, here, and here), plus easier build extensibility and some performance diagnostics.

We didn't have time, unfortunately, to address converting the solution file to MSBuild (#3) – which we would dearly love to do – nor to add a Visual Studio integrated debugger (#1).

At least, not a supported one!

Mike Stall approached us to demonstrate an ingenious reflection-emit idea which made it considerably more feasible to create an MSBuild specific debugger with many of the features of the real managed code debugger. While on leave I wrote and checked-in the code to do it. Unfortunately we couldn't complete it in time to make the 4.0 schedule.

For that reason, it's in the product, but disabled by default. It does work, it's just not supported or documented, and has a few limitations and bugs: it may be slow, it's not always pretty, and in at least one case, it's a little inaccurate. This blog post is "unofficial" documentation of how to use it in the hope it will be useful. Although it's not supported we will welcome Connect feedback, but it will likely will be moved to our backlog rather than fixed immediately. It would also be a great idea to add any bug reports and feedback to the comments on this blog post.

Debugging Walkthrough

I'm going to walk through each debugging scenario in turn.

Before you start, open Visual Studio briefly and make sure that "Just My Code" is enabled. It's essential for this to work properly:

image

There's a lot of screenshots here, but this blog is rather narrow, so some of them are distorted – you can click on them to see the full size version.

Scenario 1 – Command Line only

First, enable the undocumented "/debug" switch on MSBuild.exe by setting the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSBuild\4.0 key to have debuggerenabled=true, as I've done here with reg.exe in an elevated Visual Studio prompt:

image

You should now have these keys, assuming C: is your system drive.

image

Run MSBuild /? and you'll see the new switch has appeared.

image

We are now ready to debug.

Normally you'd be debugging some build process you've customized or authored, but for illustrative purposes I'm going to debug a brand new C# Windows Forms project. I'm going to build it with the /debug switch, and it will immediately stop:

image

In my case I get a prompt to elevate, and hit Yes:

image

Then I get the standard JIT debugging prompt. Make sure you check "Manually choose the debugging engines".

image

that causes a dialog to appear to choose the debugging engine: you want Managed only. (Mixed will work, is more clunky.)

image

And you are now debugging!

image

The first thing to notice is that we are right at the top of the first project file, the very first line MSBuild is evaluating. You are breaking in automatically at the very start, as if you started debugging a regular application with "F11".  Well, almost the very start: MSBuild already read in the environment and its other initial settings:

image

Now hit F10 and you will step line by line:

image

As you step over properties, you'll see the locals window is updating:

image

image

As you probably know, MSBuild evaluates in passes. The first pass evaluates just properties, pulling in any imports as they're encountered. Try to set a breakpoint (F9) on an item tag right now – you can't! MSBuild is unaware of them at this point.

Set a breakpoint on the <Import> tag at the bottom and run to it (F5):

image

Now step in (F11). You'll enter the file that's being imported, which in this case is Microsoft.CSharp.targets.

image

The Callstack window shows that jump like a function call, including the location in the file:

image

Of course, <Import> does not have the semantics of a function call at all. Like an #include in C++, it simply logically inserts the content of another file. But I chose to make it work this way so that you can see the chain of imports in the Callstack window and figure out your context.

By setting some more breakpoints on Imports and doing step-into, I can go deeper to illustrate:

image

To get past the property pass, given that I can't set a breakpoint on items yet, I'll use a trick. I'll Step Out repeatedly (Shift-F11) until we get to the project file again, then step to get to the next pass, which is Item Definitions. The C++ build process uses Item Definitions a great deal, but they're not very interesting for C#, in fact there's only one:

image

Use the same trick to get to the Item pass, and we'll get to the first item. I've then set a breakpoint to illustrate that I can do that now.

image

Conditional Breakpoints work too, by the way as I believe do Trace Points.

Stepping a bit further, I can see items in the locals window, and also their metadata. A small bug here -- ignore the red message, and go into "Non-public members" to see the names and values:

image

image

Sometimes you'll want to figure what a condition evaluates to at the current moment. To do that, in the immediate window, pass the condition to the function EvaluateCondition:

image

It's much the same if you want to evaluate (expand) an expression, but the function is named EvaluateExpression:

image

This is also a convenient way to see what a property value is, or what's in an item list, without navigating through the locals window. Be sure to escape any slashes, as I've done here.

The Autos window doesn't work, but Watch does:

image

In the Immediate window you can change almost any project state during the build, using the new object model. For example, I'll modify this property while I'm stopped here:

image

You can do a lot through the new object model, so it's very useful to be able to call it here.

My Watch window updated to match:

image

That's the end of what I'm going to show for debugging MSBuild evaluation.

How it works

What's happening at the high level (you can find out more from Mike's blog) is that MSBuild is pretending the script is actually VB or C#. It's doing this by emitting IL on the fly that's semantically equivalent to what it's really doing as it goes through the XML. The code of MSBuild itself is of course optimized, so Just My Code hides it, but conveniently the IL isn't optimized, so it shows up. Inside the IL MSBuild emits line directives that point to the right place in the project file, completing the trick. As for the "locals", they're actually parameters passed to functions in the IL so that they appear. EvaluateCondition and EvaluateExpression are just delegates passed the same way.

As such, a large part of the basic features you get with the regular VB/C# debugger just work. Some that don't: hovering over an expression doesn't give you the result; you can't just use the "?" syntax in the immediate window; Threads and Processes windows don't make sense; I doubt Intellitrace works. Plus, there's some of our internals leaking out in the windows here and there. But by using this trick, it was vastly less work to get the basics of an integrated debugger. I believe I spent a day or two tidying up Mike's sample code, and another three days wiring it straightforwardly into MSBuild. Creating a real debugger engine would be much more costly; and something comparable with what you get for C# would be fantastically costly, so I expect that long term, this will be the MSBuild debugging story. I hope you'll agree it's a lot better than staring at XML and logs or adding <Message> tags.

In my next post I'm going to cover
  • Debugging during the build – ie., debugging what happens inside targets, and project references;
  • Debugging a multiprocessor build;
  • Debugging the build of projects loaded into Visual Studio

See you then!

Dan

Visual Studio Project & Build Dev Lead

Leave a Comment
  • Please add 2 and 6 and type the answer here:
  • Post
  • Hi,

    I added the KEY to the registry (os: Windows Server 2008 R2 x64) but the msbuild /? doesn't give me a /debug switch :(

  • Okay, I figured out why:

    C:\Windows\Microsoft.NET\Framework64\v4.0.30319>msbuild /? - has the debug option

    C:\Windows\Microsoft.NET\Framework\v4.0.30319>msbuild /? - hasn't the debug option

    KR,

    Joe

  • fd

  • Joe, make sure you're adding the key to the 32 bit registry, not the 64 bit registry. The easiest way to do this is to open a 32 bit command prompt -- run c:\windows\syswow64\cmd.exe and then run regedit or reg.exe from that.

    I haven't actually tried debugging the 64 bit MSBuild. I'd be interested to hear if it works for you.

    Dan

  • Excellent thats really helpful

  • What's "Visual Studio Dev11"?

    Are you already working on the next version?

  • This is really helpful - thanks!

  • Great stuff. Just started playing with it.

    I ran into a problem where the debugger was not able to see the contents of the solution file. I wrote a little batch file that emitted the sln as a msbuild file and that solved the problem. Here it is for anyone else who needs it.

    ======================

    @echo off

    rem This provides a way to use VS2010 to debug a *.sln.

    rem As of 8/2010, this is an undocumented feature of VS2010

    rem setup environment

    call "C:\Program Files\Microsoft Visual Studio 10.0\VC\vcvarsall.bat" x86

    rem add registry key that turns on debugging

    reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSBuild\4.0" /v DebuggerEnabled /d true /f

    rem set env variable that forces the sln to be emitted as msbuild file

    set MSBuildEmitSolution=1

    rem invoke build in debug mode, using default target

    msbuild /debug <yourSolutionHere>.sln

    ======================

  • This is unbelievably helpful. I've wanted to do this for years. Thanks for a great tool!

  • This is really cool. It makes writing build procedures so much easier.

  • It's very nice to finally to be able to debug MSBuild projects! Also, the ability to step INTO custom tasks is incredibly useful. Thanks a bunch for that!

    However, I cam across a peculiar limitation that is extremely annoying: It seems properties that are modified by <output> are not updated in the watch windows.

    For example, in this little test project, the properties MyUser, MyClient, MyHost and MyRoot don't show up in the properties list:

    <Project xmlns="schemas.microsoft.com/.../2003"

     ToolsVersion="4.0"

     DefaultTargets="P4Test">

     <UsingTask TaskName="P4Info"

       AssemblyFile="C:\Code\BioMSBuild\bin\Debug\BioMSBuild.dll"/>

     <Target Name="P4Test">

       <P4Info>

         <Output PropertyName="MyUser" TaskParameter="User" />

         <Output PropertyName="MyClient" TaskParameter="Client" />

         <Output PropertyName="MyHost" TaskParameter="Host" />

         <Output PropertyName="MyRoot" TaskParameter="Root" />

       </P4Info>

       <Message Text="P4 Info - User: $(MyUser) "/>

       <Message Text="Client: $(MyClient) "/>

       <Message Text="Host: $(MyHost)  "/>

       <Message Text="Root: $(Root)" />

     </Target>

    </Project>

    However, the messages are printed as expected, with the values retrieved from the task.

    I've tried predefining the properties in the project:

    <PropertyGroup>

     <MyUser>user</MyUser>

     <MyClient>client</MyClient>

     <MyHost>host</MyHost>

     <MyRoot>root</MyRoot>

    </PropertyGroup>

    They then appear in the properties list but the values shown are never updated. Still the messages are displayed properly.

    If the properties are predefined within a target, they don't show up in the properties list.

    They also show up as non-existent in the intermediate window:

     EvaluateExpression("'$(MyUser)'")

     "''"

    In my view, this is quite important to be able to inspect values as they are modified at runtime and I'm sure you thought about this. So is this something I missed or is this a known limitation? If it's a limitation, do you plan to provide a fix for this? And if so can we expect it with VS2010 SP1?

    Cheers!

    joce.

    P.S. (Sorry if you got this post multiple times... It just disappeared when I pressed send... I've retyped and am trying to send it again)

  • Hello, Dan!

    I can't get it to work. I am using Windows XP with VS 2010 on a x86 machine. I added the registry key, closed regedit, but no /debug option appears on devenv.

    What could be the problem?

    Regards,

    Ricardo

  • Forget it... my fault: msbuild, not devenv! :-(

    It works!

  • It does not work for me. I have the following msbuild.

    Microsoft (R) Build Engine Version 4.0.30319.1

    [Microsoft .NET Framework, Version 4.0.30319.1]

    Copyright (C) Microsoft Corporation 2007. All rights reserved.

  • Hmm, I couldn't get to the debugger because it was looking for a metaproj file. My sln doesn't have any dependencies that I can see. Any other things that could cause this?

Page 1 of 2 (24 items) 12