A very common problem to solve with an MSBuild script is deleting unwanted
files from your build directory. Common files that you may not wish to distribute
with your application include XML files, Unit Test DLLs or mock libraries that were
in your solution, and PDB debugging files.
For several days, my build script did this flawlessly; every other time it
was executed. Now there’s a PITA for you. (Look it up.)
I puzzled over this build script of mine until I realized that this was primarily
a state problem, which we can describe with the following observations.
-
My script was placing all compilation artifacts into a single build folder and
then deleting the unwanted items. After this, the remaining artifacts were copied
to a deployment location. This is common behavior for a Team Build Script, such
as this one. -
I specifically authored and called the target that deleted my unwanted items.
I called it: DeleteDebugFiles -
The list of files to be deleted was being created as a single item group outside of
the custom target, and then used within the Delete task.
Here is what the build script looked like.
<ItemGroup>
<DebugFilesToDelete Include="$(AcceleratorBinaries)\*Test*.dll"/><DebugFilesToDelete Include="$(AcceleratorBinaries)\**\*.pdb"/><DebugFilesToDelete Include="$(AcceleratorBinaries)\*.xml"/><DebugFilesToDelete Include="$(AcceleratorBinaries)\nunit.*"/><DebugFilesToDelete Include="$(AcceleratorBinaries)\Rhino.*"/>
</ItemGroup>
<Target Name="DeleteDebugFiles">
<Message Text=" ==== Deleting unwanted files that are only used for testing or debugging. ==== "/><Delete Files="@(DebugFilesToDelete)" ContinueOnError="false"/><Message Text=" ==== Deleted ==== "/>
</Target>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
So what was the problem? All looks well with this code and in fact, it worked
perfectly 50% of the time.
The Problem
The problem with my code is the <ItemGroup> is evaluated before
the target in the MSBuild script that actually fires the compile. This means
that when this script STARTS it looks into the build output folder $(AcceleratorBinaries)
and includes the files that it finds via the masking patterns in the paths.
This ItemGroup is populated before the compilation, so on the first run the files
that we want to delete are not yet present, therefore the ItemGroup remains empty.
By the time our ItemGroup actually gets called for use in the DeleteDebugFiles
target, it is still empty.
On the 2nd run, the Debug files from the first compile are still there because I didn’t
clean out $(AcceleratorBinaries) at the end of the first run. So this time the
ItemGroup is populated as we want (or so it appears) but the files it read are
the ones from run #1. Run #2 deletes the files, setting up run #3 to have the
same problem as run #1. And so on…
How to Fix It
Obviously, we don’t want to evaluate the ItemGroup until we are ready to actually
use it. The best way I could find to do this looks like this:
<Target Name="BeforeDropBuild" DependsOnTargets="ZipTemplateLibraries"> <Message Text=" ==== Deleting unwanted files that are only used for testing or debugging. ==== "/> <Message Text="Deleting *.pdb files"/> <CreateItem Include="$(AcceleratorBinaries)\*.pdb"> <Output ItemName="PDBFilesToDelete" TaskParameter="Include"/> </CreateItem> <Delete Files="@(PDBFilesToDelete)" ContinueOnError="true"/> <Message Text="Deleting *.xml files"/> <CreateItem Include="$(AcceleratorBinaries)\*.xml"> <Output ItemName="XMLFilesToDelete" TaskParameter="Include"/> </CreateItem> <Delete Files="@(XMLFilesToDelete)" ContinueOnError="true"/> <Message Text="Deleting Test DLL files"/> <CreateItem Include="$(AcceleratorBinaries)\*Test*.dll"> <Output ItemName="DLLFilesToDelete" TaskParameter="Include"/> </CreateItem> <Delete Files="@(DLLFilesToDelete)" ContinueOnError="true"/> <Message Text=" ==== Done Deleting ==== "/> </Target>
This technique causes us to perform separate
deletes for each file mask because we are creating single Items, not ItemGroups.
The scope of the Items is within the target, not peer to the target, therefore evaluation
of the Items to delete does not occur until the target is called.
Thanks, Butch. I found your example in
source-control :).
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Thanks a ton. I’ve been searching for the workaround for a couple of days now.