Friday, January 14, 2011

Nested ItemGroups in MSBUILD

First something about what I want to achieve. In our setup we're using boost C++ library quite intensely and therefore we would like the code and the build process to be part of our TFS setup. We don't use the binary libraries out of the box because for example in Visual Studio 2005 and 2008 a release build was built by default with checked iterators (macro _SECURE_SCL = 1) and our code was build without checked iterators. Boost only supported the Visual studio defaults, so we have a long tradition of building our own boost libraries. The default checked iterator policy was changed back in Visual Studio 2010 to disabled in release mode, but we maintain both flavours.
Since Visual studio 2010 we started using TFS and TFS build. The custom MSBUILD project to produce the boost binaries had the following structure

<ItemGroup>
  <BoostLib Include="regex"/>
  <BoostLib Include="thread"/>
  <BoostLib Include="filesystem"/>
  <BoostLib Include="date_time"/>
  <BoostLib Include="random"/>
</ItemGroup>
<ItemGroup>
  <BuildPlatform Include="Win32">
     <AddressModel>32</AddressModel>
  </BuildPlatform>
  <BuildPlatform Include="x64">
    <AddressModel>64</AddressModel>
  </BuildPlatform>
</ItemGroup>
<ItemGroup>
  <BuildVariant Include="Debug">
    <CxxFlag>\D_SECURE_SCL=1</CxxFlag>
    <Variant>debug</Variant>
  </BuildVariant>
  <BuildVariant Include="Release">
    <CxxFlag>\D_SECURE_SCL=1</CxxFlag>
    <Variant>release</Variant>
  </BuildVariant>
  <BuildVariant Include="ReleaseSecure0">
    <CxxFlag>\D_SECURE_SCL=0</CxxFlag>
     <Variant>release</Variant>
  </BuildVariant>
</ItemGroup>
<PropertyGroup>
  <_CommonCmdLine>$(ProjectDir)..\bjam.exe toolset=msvc-10.0 link=static threading=multi runtime-link=static --cxxflags=/MP</_CommonCmdLine>
</PropertyGroup>
This gave me maximum flexibility: I can add or remove libraries, build variants or address models. But now I had to call bjam for each combination. And this is not trivial in MSBUILD.

I have found one reference to combining or nesting Items from ItemsGroups, but sadly this doesn't seem to work for me. If I attach the follwoning target, as suggested in the blog


<Target Name="Build">
    <PropertyGroup>
      <CurrentVariant>%(BuildVariant.CxxFlag)</CurrentVariant>
    </PropertyGroup>
    <Message Text="VariantPlatform $(CurrentVariant) : %(BuildPlatform.FileName)"/>
</Target>

The output I get reads

So the property CurrentVariant gets all the subsequent BuildVariant values and in my Message task I'm stuck with the last value.

I will not bother you with all the combinations and tricks I have tried but in the end I experimented with CreateItem and came up with the idea to batch over one type item while adding AddionalMetadata from another type. Something like this



<Target Name="Build">
   <CreateItem
      Include="@(BuildVariant)"
      AdditionalMetadata="Platform=%(BuildPlatform.FileName);AddressModel=%(BuildPlatform.AddressModel)">
      <Output
        TaskParameter="Include"
        ItemName="VariantPlatform"/>
     </CreateItem>
    <Message Text="VariantPlatform %(VariantPlatform.Platform), %(VariantPlatform.FileName), %(VariantPlatform.AddressModel)"/>
</Target>

This gave the following result
This was exactly what I was after: a combination of the two properties!


For completeness here is the rest of the  MSBUILD projects which builds the boost binaries

<Target Name="ComposeItems" BeforeTargets="Build;Clean;Rebuild">
   <CreateItem
     Include="@(BuildVariant)"
     AdditionalMetadata="Platform=%(BuildPlatform.FileName);AddressModel=%(BuildPlatform.AddressModel)">
       <Output
         TaskParameter="Include"
          ItemName="VariantPlatform"/>
   </CreateItem>
   <Message Text="VariantPlatform %(VariantPlatform.Platform) : %(VariantPlatform.FileName) ? %(VariantPlatform.AddressModel)"/>
   <CreateItem
     Include="@(VariantPlatform)"
     AdditionalMetadata="Lib=%(BoostLib.FileName)">
        <Output
         TaskParameter="Include"
         ItemName="Tobuild2"/>
       </CreateItem>
    <CreateItem
       Include="@(Tobuild2)"
       AdditionalMetadata="IntermDir=$(INTERMEDIATEROOT)\Boost_$(Boostver)\%(Tobuild2.Platform)\%(Tobuild2.FileName); OutDir=$(OUTPUT)%(Tobuild2.Platform)\%(Tobuild2.FileName)">
       <Output
       TaskParameter="Include"
       ItemName="Tobuild"/>
       </CreateItem>
         <Message Text="BUILD %(Tobuild.Lib) %(Tobuild.Platform) : %(Tobuild.FileName) ? %(Tobuild.AddressModel) %(ToBuild.StageDir)"/>
     </Target>
     <Target Name="Build">
     <Exec WorkingDirectory="$(ProjectDir)boost_$(BOOSTVER)"
Command='$(_CommonCmdLine) variant=%(Tobuild.Variant) cxxflags=%(Tobuild.CxxFlag) "--stagedir=%(ToBuild.OutDir)" "--build-dir=%(ToBuild.IntermDir)" --with-%(ToBuild.Lib) stage'/>
     </Target>
     <Target Name="Clean">
     <Exec WorkingDirectory="$(ProjectDir)boost_$(BOOSTVER)"
Command='$(_CommonCmdLine) --clean variant=%(Tobuild.Variant) cxxflags=%(Tobuild.CxxFlag) "--stagedir=%(ToBuild.OutDir)" "--build-dir=%(ToBuild.IntermDir)" --with-%(ToBuild.Lib) stage'/>
     </Target>
     <Target Name="Rebuild">
     <Exec WorkingDirectory="$(ProjectDir)boost_$(BOOSTVER)"
     Command='$(_CommonCmdLine) -a variant=%(Tobuild.Variant) cxxflags=%(Tobuild.CxxFlag) "--stagedir=%(ToBuild.OutDir)" "--build-dir=%(ToBuild.IntermDir)" --with-%(ToBuild.Lib) stage'/>
   </Target>
Happy programming!