A Blog

Big msbuild gotcha

September 13, 2007

Msbuild is not necessarily a “real” component of TFS, but it is heavily integrated throughout its build system (as well as Visual Studio 2005), so I’ll be recording my thoughts on it as well. It seems to be better than NAnt, a similar open source product, in most regards, but I have spent the better part of a day getting to the bottom of one of its maddening quirks.

You can define an <ItemGroup> consisting of files that you can copy, zip up, or do whatever you want with, but the collection it will only be evaluated at the initialization of the script!

For example, say you need to deploy all the files compiled by a solution, and then do something else with the binaries. Your build script may look something like this:

  &lt;ItemGroup&gt;
        &lt;WebDeploy Exclude="@(ExcludedTypes)" Include="**\**"&gt;
        &lt;BinDeploy Include="$(DeployDir)\bin\*.dll"&gt;
&lt;/ItemGroup&gt;

&lt;Target DependsOn="Compile" Name="Deploy"&gt;
        &lt;Copy DestinationFolder="$(DeployDir)\%(RecursiveDir)" SourceFiles="@(WebDeploy)"&gt;
        &lt;Copy DestinationFolder="$(BinDeployFolder)" SourceFiles="@(BinDeploy)"&gt;
&lt;/Target&gt;

The @(BinDeploy) group will evaluate <em>at the start of the script</em>. This means that if you have a "clean" folder setup, the group will be empty, even after the Copy task has been executed. Additionally, if you run the script a second time without deleting the contents of the $(DeployDir), the script will happily copy over the prior build's binaries (even if a Delete task has "deleted" them, and they've been "replaced" by a fresh compile).

To fix this problem, you need to use the <a href="http://msdn2.microsoft.com/en-us/library/s2y3e43x.aspx" title="CreateItem task reference on MSDN">CreateItem</a> task:

<ItemGroup> <WebDeploy Exclude=”@(ExcludedTypes)” Include=”\”>

<Target DependsOn=“Compile” Name=“Deploy”> <Copy DestinationFolder=”$(DeployDir)%(RecursiveDir)” SourceFiles=”@(WebDeploy)”> <CreateItem Include=”$(PathToCompiledBinaries)*.dll”> <Output ItemName=“BinDeploy” TaskParameter=“Include” /> </CreateItem> <Copy DestinationFolder=”$(BinDeployFolder)” SourceFiles=”@(BinDeploy)”> </Target>

This will guarantee that @(BinDeploy) isn't created until there are files for it. Do note that this "feature" happens for everything that uses ItemGroups this way; it's not just limited to the Copy task.

Scott Williams

Written by Scott Williams who lives and works in sunny Phoenix, AZ. Twitter is also a place.