Supporting VS.NET and NAnt
Up until recently, I'd been running automated builds for .NET apps using a set of batch files that called out to devenv.exe to do the actual build. This worked well at first, but has a number of drawbacks, some of which I detailed in a previous post. In addition, batch files give you lousy error handling, in that failures must be manually caught and handled via ERRORLEVEL statements. Furthermore, batch files are difficult to debug and test. Finally, the system I'd created wasn't very extensible; I had to manually add boilerplate each time I needed to add projects to the master build, and the system had become a maze of calls to other batch files, Windows scripts, and one-off executables. Thus, when I got the tastk to integrate 2 new web service solutions into the build, I decided it was time to bite the bullet and go to NAnt.
The big obstacle to adopting NAnt wasn't necessarily writing the scripts, though, it was ensuring that the master build stayed in sync with the Visual Studio solutions. While I'm not opposed to command line compiles, I wasn't willing to give up the productivity boost I get from VS.Net, and I wasn't going to ask the rest of the team to either (I already knew what the answer would be). The SLiNgshoT program, now part of NAntContrib, is one way to do this. SLiNgshoT works pretty well, but I found that I didn't care for the style of build file it creates. Fortunately, VS.Net stores project information in XML format, so an XSLT transform can be used to create a NAnt .build file.
The first task was to determine the format of the basic build file. There's several articles at John Lam's site dealing specifically with NAnt: Effective NAnt, Build scripts and debug/release builds, More C++ and NAnt Stuff, Building C++ projects with NAnt, and a couple articles on John's VersionTask. There's also Steve Loughran's Ant In Anger. I had contacted Steve via email and he also alerted me to his upcoming book, Java Development With Ant. I had the opportunity to look at a couple chapters of this, and it looks like a good one. This is one nice thing about NAnt, that much of what applies to Ant, applies to NAnt. Here's the general format I settled on:
<project default=Test>
<target name="init-Debug" if="${debug}"/>
<target name="init-Release" unless="${debug}"/>
<target name="init" depends="init-Debug, init-Release">
<target name="compile" depends="init" />
<target name=Test depends="init,link"/>
<target name="link" depends="init,compile">
<target name="package" depends="init">
<target name="clean" depends="init"/>
</project>
There's more targets to handle my specific needs, but these are mostly boilerplate. The targets listed are the ones whose contents are driven by the input project file.
The other thing that I did is to create a NAnt buildfile to drive the transformation process (available here). Basically, this is a 3 step process: prepare, convert (using the <style> task) and test. test is the default target, and the test consists simply of running the generated buildfile using the <nant> task. Pretty simple: if the build succeeds, the test passes. One gotcha on the <style> task: it will fail if the input file is read only.
So now the question is, how do I populate the build shell? The general format for a C# or VB project file is this:
<VisualStudioProject>
<CSHARP> <!-- or VisualBasic -->
<Build>
<Settings>
<Config Name = "Debug" />
<Config Name = "Release"/> <!-- more targets -->
</Settings>
<References>
<Reference /> <!-- more references -->
</References>
</Build>
<Files>
<Include>
<File /> <!-- more files -->
</Include>
</Files>
</CSHARP>
</VisualStudioProject>
Now, I'll introduce the transform itself. You can look at the file on the side while I describe some of the high points. This transform is a stripped down version of what I use at work, removing a lot of the cruft that's specific to our environment.
The template that matches "VisualStudioProject" is the top level for the transform. At this point, I set up the top level NAnt <project> element and set the name attribute to match the name of the VS.NET project. There's a choice where I check to see if this is a C++ project and explicitly call out to the "Cxx" template if it is. I'll ignore C++ for the remainder of this discussion, because it's totally different from VB and C#.
The next level that will be matched is the CSHARP or VisualBasic elements. Both of these work the same way; they call templates to generate boilerplate prolog and epilog, with an eval-templates sandwiched in between. eval-templates passes a parameter named 'compiler' which indicates the type of compiler to be used, we'll need it later.
The template matching the VS.NET <Build> element will be evaluated next, and the fun really starts. The first step is to apply-templates on the <Settings> child. <Settings> tells us general information about the project that applies to all configurations, so I create the init target there and save off the settings into properties. The tricky part about the init target is to make it rely on conditionally applied targets; one target for each <Config> child of <Settings>. Once I've generated the init target, I apply-templates on the <Config> children. As I mentioned, these are conditionally executed. In this version, I generate a global property named "debug" - if this config refers to a release build, the condition is unless="${debug}", if it's a debug build, if="${debug}". This works for the default VS.NET project, but if you have more configurations than this, you'll probably want some more complex conditions.
So we're finished with <Config> and the transform pops context to <Build>. Now, we apply-templates on the <References> child, but using the "tlbimp" mode. This is to handle the one complication I encountered with VB and C# projects, namely, how to handle COM object references. The trouble is that VS.NET stores only the GUID of the type library you're referencing, but the tlbimp tool expects the path. My solution to this was to build a task to look up the path of the type library, using the Win32 QueryPathOfRegTypeLib function, and store the path in a property to be passed to the NAntContrib <tlbimp> task. This template generates a target for each type library, each target containing the <script> task to convert the guid to a path, and a <tlbimp> task to create an interop wrapper for the COM object. Again, context pops back to <Build>.
The final part of the template for the <Build> element is to invoke the actual compile task. Recall from the earlier discussion that the name of the compiler is passed to the template as a parameter. This works well because the <csc> and <vbc> tasks take the same attributes, though <vbc> has a few more options. NAnt will ignore attributes that a task doesn't define, so I don't worry about filtering out VB - specific options. Most of the construction of the compile task is straightforward, though I check to see if there's an Application Icon defined before adding that attribute - you don't want to pass an empty string for this. The last part of the task builds the <sources>, <resources> and <references> elements, by evaluating the <Files> and <References> elements, this time without a mode. For <Files>, I evaluate the children twice, first to gather the sources, and then to build the resources. With references, if the reference is a project reference, I assume you can look for it in the ${lib} directory, and for COM references, I set the reference to look for the output of the <tlbimp/> task for that library.
Once the template for <Build> is finished, context pops up to the parent element. I mentioned that these templates generate some boilerplate epilog code as well. This consists of the "build", Test, "clean" and "package" targets, of which package is the only interesting one. A package target is generated only if the project is an executable or Web application. In my example, I simply copy the output of the build along with all the dependent projects' output and COM Interop wrappers the build generated, along with files marked as "content", to another directory. You could put all this into a zip file, or use the MSI task from NAntContrib instead.
I like the XSLT approach because it offers flexibility in tailoring your output to your specific needs. For our web services, the build is significantly different to account for our configuration. The weakness to this approach is that .sln files are not XML, so you have to manually create a top level buildfile and invoke all the child builds in the correct order. I didn't find this to be too horrible to do in my case, but one possible solution would be to reuse the SLiNgshoT code that parses the .sln file, but substitute a transform for the part that handles project files. Overall, this transform has come in very handy for me. This week, I was asked to add a new solution into our master build the day before we did a deployment; using my transform, I had the solution added in 5 minutes. The deployment's a story for another day...