Recursive msbuild

Today a co-worker asked me a really interesting thing about msbuild. He wanted to extract some information about the binary he just compiled into a textfile and add that file as a resource to the binary he just compiled. It is worth mentioning he had a really good reason to do this as well.

The good reason and the fact that it was quite an interesting question got me thinking on how to do that with minimal effort. Minimal effort is always a good sign of a lazy programmer, and lazy programmers are better. At least that’s what I read somewhere :)

First of all, it wouldn’t be possible to simply call the target Build again, as msbuild doesn’t invoke the same target twice in one run.

I figured it’d be necessary to call msbuild again, and in the recursively invoked msbuild to all the tricks.

First off, I wrote a simple program to extract some information from a binary file.

namespace GenApi
{
    using System.Reflection;

    class Program
    {
        static void Main(string[] args)
        {
            var a = Assembly.LoadFrom(args[0]);
            foreach (var type in a.GetTypes())
            {
                Console.WriteLine(type.FullName);
            }
        }
    }
}

Not overly complex, but good enough for demonstration purposes. I compiled this to genapi.exe and moved it to the root folder of the project I wanted to tamper with.

Msbuild is a quite powerful system, and it has numerous extension points. One of these is to override – or actually redeclare – a target named AfterBuild. This target is invoked after the chain of targets pertaining to build is complete. The default implementation of AfterBuild is to do nothing.

At this point I need to mention the importance of understanding the build system used. There are quite a number of problems that can and should be solved by extending and enhancing the build process. The first step on that journey to enlightenment is to start thinking about the build system as part of the product development infrastructure. It is not just some mumbo-jumbo mixed with two bottles of magic that happened to ship with .Net and Visual Studio. It is an orchestration environment of quite some power.

Anyway, start with right-clicking the project node in the solution explorer and select Unload project. Right-click the same node again and select ‘Edit…’. You’ll now get the csproj file as a textfile in your source editor in VS.

The end of the project file looks like

 <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">


  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

This is exactly what we want to do. First, to try the waters, I wrote a simple AfterBuild target just to make sure it got executed.

<Target Name="AfterBuild">
   <Error Text="Yes, AfterBuild works..."/>
</Target>

Reload the project file, and turn on more logging by selecting Tools | Options | Projects and Solutions | Build and Run and go for Normal in both the combo-boxes. View output from build by Ctrl-W, O.

Rebuild and you’ll get something like

GenerateTargetFrameworkMonikerAttribute:
Skipping target "GenerateTargetFrameworkMonikerAttribute" because all output files are up-to-date with respect to the input files.
CopyFilesToOutputDirectory:
  IncludeMe -> c:\users\jhm\documents\visual studio 2010\Projects\SelfInclusion\IncludeMe\bin\Debug\IncludeMe.exe
c:\users\jhm\documents\visual studio 2010\Projects\SelfInclusion\IncludeMe\IncludeMe.csproj(56,5): error : Yes, AfterBuild works...

Build FAILED.

Great. Build failed.

Now let’s do some serious work.

What we need is to generate the “api”. The target for this looks like

 <Target Name="GenerateApi"
          Inputs="$(OutputPath)\$(AssemblyName).exe"
          Outputs="api.txt" >

    <Exec Command="genapi.exe $(OutputPath)\$(AssemblyName).exe &gt; api.txt" />
  </Target>

The generated file, ‘api.txt’ needs to be included. We could in theory add it to the project once we have it, but imho it makes much more sense to only add it after it is generated. Thus, we need to modify the Resource item. To avoid doing that too early, this must be done in a target as well. Manipulating items and properties in targets is msbuild’s way to handle deferred evaluation.

 <Target Name="AddResource">
    <ItemGroup>
      <Resource Include="api.txt" />
    </ItemGroup>
  </Target>

Now what with the afterbuild target? There are many ways to skin a cat, and I opted for two invocations of the csproj file, first to generate the resource, and then to rebuild the project with the freshly generated file.

 <Target Name="AfterBuild">
    <MSBuild Projects="Includeme.csproj" Targets="GenerateApi"/>
    <MSBuild Projects="Includeme.csproj" Targets="Build"/>
  </Target>

However, the code above suffers from a serious mistake. When msbuild is invoked for the second time, it will surely call build, but it will also end up in the AfterBuild-target and call itself again. Infinite recursion. Not good. To prevent this from happening, we need to pass a token indicating we’re in level two, and also check that before executing anything. Improved version looks like

 <Target Name="AfterBuild">
    <MSBuild Projects="Includeme.csproj" Targets="GenerateApi"
       Condition="'$(second)' == ''"/>

    <MSBuild Projects="Includeme.csproj" Targets="Build"
       Properties="second=true" Condition="'$(second)' == ''" />

  </Target>

Much better! However, when are we calling the AddResource-target? To fix that we need to do some indirection, as msbuild doesn’t seem to commit item changes to global scope until a target is completed. Thus, instead of invoking the Build-target, we invoke a new target named SecondBuild. It in turn depends on AddResource and Build.

<Target Name="SecondBuild" DependsOnTargets="AddResource;Build"/>

The complete code that works thus looks like:

 <Target Name="AfterBuild">
    <MSBuild Projects="Includeme.csproj" Targets="GenerateApi" Condition="'$(second)' == ''"/>
    <MSBuild Projects="Includeme.csproj" Targets="SecondBuild" Properties="second=true" Condition="'$(second)' == ''" />
  </Target>
  <Target Name="SecondBuild" DependsOnTargets="AddResource;Build"/>
  <Target Name="GenerateApi"
          Inputs="$(OutputPath)\$(AssemblyName).exe"
          Outputs="api.txt" >

    <Exec Command="genapi.exe $(OutputPath)\$(AssemblyName).exe &gt; api.txt" />
  </Target>
  <Target Name="AddResource">
    <ItemGroup>
      <Resource Include="api.txt" />
    </ItemGroup>
  </Target>

Staying true to the agile programming cycle, this is where one should pause and refactor. Could the code be written any cleaner? Do we need two invocations of msbuild? As it turns out, no, not really. The same effect can be achieved with the following, more compact version:

 <Target Name="AfterBuild">
    <MSBuild Projects="Includeme.csproj" Targets="GenerateApi;AddResource;Build" Properties="second=true" Condition="'$(second)' == ''" />
  </Target>
  <Target Name="GenerateApi"
          Inputs="$(OutputPath)\$(AssemblyName).exe"
          Outputs="api.txt" >

    <Exec Command="genapi.exe $(OutputPath)\$(AssemblyName).exe &gt; api.txt" />
  </Target>
  <Target Name="AddResource">
    <ItemGroup>
      <Resource Include="api.txt" />
    </ItemGroup>
  </Target>

There you go. A simple way to include information about a binary in the binary itself. All from one Build request from inside VS.

A very interesting question and a fairly simple and clean answer.

Happy building!

–Jesper

Share

Leave a comment

Your comment