In the Trenches with MSBuild, Part II

I took some time last weekend to work on my build system again. My last post on the subject was aimed primarily at pointing out some of the issues and idiosynchrasies I uncovered trying to revamp the existing build process. This post, drawing upon what I learned the last time around, will focus on illustrating how I solved or worked around those problems to get the result I’d intended from the start.

This time around, I was focusing my efforts on more complete builds; builds that would be run as part of a continuous integration step, or as part of a release cycle. These sorts of builds do things that the developer’s daily workflow build doesn’t really need to bother with — produce and compile documentation, execute unit tests, compile and validate content, and so on. “Slow” tasks, basically.

The first thing I did, actually, was unrelated to MSBuild — I knocked together an XSL transformation that turned the XML documentation output from the C# compiler into plain text files containing wiki markup for DokuWiki, so that I could integrate the API documentation with my broader documentation. That, however, is a topic for a later post (once I perfect it, it’ll be going into SlimDX as well).

The things I had wanted to accomplish last time around, but failed to do, were as follows:

  1. Determine which projects are classified as “products” (things I would ship) and which projects are “tests” (for unit testing), and build them using the same process used as part of the developer’s working build.
  2. Collect the documentation files produced from the previous step, and run them through the XSL transform to get something I could import into the wiki.
  3. Collect the test assemblies, place them in a staging area and execute the tests they contain.
  4. Collect the product assemblies, place them in a staging area that mimics the final installation structure.

Accomplishing the first item was easy enough using the RegexMatch task provided by the MSBuild Community Tasks project (MSBuild itself doesn’t seem to have support for an obvious “string contains” type of conditional), as all of my test projects follow a simple naming convention: Foo.Bar.Tests.csproj is the test project for Foo.Bar.csproj. I simply apply the regex to the item collection containing my project files, and I get back an item collection containing my test projects. Using that, I can perform what is effectively a set difference operation between the projects collection and the test projects collection to yeild the collection containing only product projects. The targets look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
<Target Name="FindTestProjects">
  <RegexMatch Input="@(Projects)"
              Expression="Tests">
    <Output ItemName="TestProjects" TaskParameter="Output"/>
  </RegexMatch>
</Target>
 
<Target Name="FindProductProjects" DependsOnTargets="FindTestProjects">
  <CreateItem Include="@(Projects)"
              Condition="'%(Identity)' != '@(TestProjects)'">
    <Output ItemName="ProductProjects" TaskParameter="Include"/>
  </CreateItem>
</Target>

Remember, since I’m publishing item groups I want to re-use later on (when I actually build the relevant projects), and since item groups are not published until after the target completes, I need to have two seperate targets here. In case you’re wondering, @(Projects) is a static item group using wildcards to grab every .csproj in my projects subdirectory; nothing too fancy.

Actually building the projects is fairly straightforward, and I don’t actually require the partitioned item collections for this stage. I simply feed everything in the @(Projects) collection to the MSBuild task, which assures me that they are build the same way as they would be from within the IDE. Since building the projects will create their documentation XML (if they are configured to do so), I also capture any of those files into a new item collection called @(Documentation).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Target Name="BuildProjects" DependsOnTargets="FindTestProjects;FindProductProjects">
  <MSBuild Projects="@(Projects)"
           Properties="Configuration=$(Configuration);Platform=$(Platform)"
           />
 
  <CreateItem Include="@(TestProjects->'%(RelativeDir)\bin\$(Configuration)\*.*')">
    <Output ItemName="TestArtifacts" TaskParameter="Include"/>
  </CreateItem>
 
  <CreateItem Include="@(ProductProjects->'%(RelativeDir)\bin\$(Configuration)\*.*')">
    <Output ItemName="ProductArtifacts" TaskParameter="Include"/>
  </CreateItem>
 
  <CreateItem Include="@(Projects->'%(RelativeDir)bin\$(Configuration)\%(FileName).xml')"
              Condition="Exists('%(Projects.RelativeDir)bin\$(Configuration)\%(Projects.FileName).xml')">
      <Output ItemName="Documentation" TaskParameter="Include"/>
  </CreateItem>
</Target>

You can see that I’m making the assumption that the documentation XML is generated in a specific location with a specific file name (that matches the file name of the .csproj). This does technically violate my requirement that configuration changes made in the IDE should not alter or break the out-of-IDE build. However, there isn’t a particularly clean way to pull the name of the documentation file all the way out of the MSBuild task (the documentation file is, after all, specific to certain applications of MSBuild projects). In the end, this is more straightforward — and doesn’t matter too much since nothing is done with the documentation file for the developer build.

Finally, I build the documentation with a simple target that uses the Exec task to call my XSL tool on each @(Documentation) item, and then copy everything to the appropriate staging directories (doing this required those “products” and “tests” item lists, as I didn’t want to mix the two). Even executing the unit tests was pretty simple, using the community-provided NUnit task, although I’ll probably be doing some customization in the future to wrangle the test results report and so on.

Some Thoughts

It’s certainly a lot easier to work with MSBuild outside of the IDE integration, where experimentation with the build involves a painful unload-reload-test cycle, because of issues I touched on before. It struck me, as I was writing this, that another reason I found MSBuild so painful initially was that — since I was working with the in-IDE builds — I wanted to leverage properties and the like that were published by Microsoft.Common.targets or Microsoft.CSharp.targets, the files that contain the meat of the Visual Studio build process. Whether because you aren’t actually supposed to be relying on the values those targets create, or because nobody has gotten around to it yet, documentation of those files (and within them) is pretty slim. They’re also written by MSBuild wizards, presumably, which means they can at times be rather like gibberish to somebody who’s just getting his feet wet, like myself.

There are still some things that I don’t fully understand that I can’t find good explainations of — for example, I wrote the set-difference condition '%(Identity)' != '@(TestProjects)' purely on the assumption that comparing a metadata property (%(Identity)') in that context would resolve to the metadata of each item in the input item list and also that comparing that property, which is a scalar value, against an array value would compare it against each scalar value in that array. It yeilded the results I expected and wanted in this case, but I’m not too clear whether or not what I assumed is going on is, in fact, what is really going on. Confirming these sorts of details is tricky.

In any case, my overall opinion of the tool is moving in a more positive direction, even though the work I did this weekend wasn’t particularly difficult. Next time I return to this domain, it will likely be to clean up and optimize the process a bit, and perhaps add content build integration.

Footnote

The parser in the source code display plugin I’m using apparently doesn’t handle the nested quotes that show up frequently in MSBuild all that well. I’ll look into it.