Share via



February 2009

Volume 24 Number 02

MSBuild - Best Practices For Creating Reliable Builds, Part 1

By Sayed Ibrahim | February 2009

In this article, the first of two parts, I describe several best practices that developers should follow when using MSBuild, the Microsoft build engine used by Visual Studio to build managed projects. In this first part, I'll cover some basic practices and techniques that you can apply to most every project. I'll also discuss topics such as defining task dependencies, batching tasks, organizing targets, and more. In Part 2, I'll discuss some practices related to build configurations that require heaver customization, largely because of their size.

If you are not familiar with MSBuild, take a look at my previous articles published in MSDN Magazine: "Inside MSBuild: Compile Apps Your Way with Custom Tasks for the Microsoft Build Engine" and "WiX Tricks: Automate Releases with MSBuild and Windows Installer XML."

Defining Target Dependencies

When you define a target in MSBuild, you can use the DependsOnTargets attribute to define target dependencies. MSBuild uses this attribute to determine the order in which targets are executed. For example, if you define a target named Deploy that depends on the targets PrepareForBuild and Build, you can express that dependency as follows:

<Target Name="PrepareForBuild">
  <!-- target contents here -->
</Target>
<Target Name="Build">
  <!-- target contents here -->
</Target>
<Target Name="Deploy" DependsOnTargets="PrepareForBuild;Build">
  <!-- target contents here -->
</Target>

Although the previous listing defines the dependencies you want, it does not provide a way to extend the behavior of the Deploy target. It would be better if you could specify additional targets that the Deploy target depends on without having to modify the Deploy target itself. You can implement this behavior by defining properties inside the DependsOnTargets attribute instead of using hardcoded values.

Take a look at the modified Deploy target shown here:

<PropertyGroup>
  <DeployDependsOn> $(DeployDependsOn); PrepareForBuild; Build </DeployDependsOn>
</PropertyGroup>
<Target Name="Deploy" DependsOnTargets="$(DeployDependsOn)">
  <!-- target contents here -->
</Target>

In this version, the Deploy target depends on the targets contained in the DeployDependsOn property. (By convention, these types of properties are named with the target name followed by DependsOn, but this is not required.) Consumers can now attach additional targets to the DeployDependsOn property by overriding it. However, notice that the declaration of the DeployDependsOn property contains a reference to itself: $(DeployDependsOn). Including this reference ensures that any existing values for DeployDependsOn are not overridden. Instead, they are simply appended.

This approach is better, but it only supports adding targets that are executed before the target in question. If you want consumers to be able to specify targets that should be executed either before or after a target, you need to declare the Deploy target, as shown in Figure 1.

 Figure 1 Deploy Target Declaration

<PropertyGroup>
  <DeployDependsOn> $(DeployDependsOn); PrepareForBuild; Build; BeforeDeploy; CoreDeploy; AfterDeploy </DeployDependsOn>
</PropertyGroup>
<Target Name="Deploy" DependsOnTargets="$(DeployDependsOn)"/>
<Target Name="BeforeDeploy"/>
<Target Name="AfterDeploy"/>
<Target Name="CoreDeploy">
  <!-- target contents here -->
</Target>

In this case, the Deploy target is an empty target. The only thing it does is specify the targets that it depends on. The actual work is performed inside the CoreDeploy target. By taking this step, you can extend the DeployDependsOn property to execute targets both before and after CoreDeploy executes.

Additionally, there are two other empty targets here: BeforeDeploy and AfterDeploy. They are declared to allow consumers a quick way to extend the Deploy target. If either of these targets is defined by a consumer, it will be called at the appropriate time. The difference between overriding a target like BeforeDeploy and extending the DeployDependsOn property is that only one definition of the BeforeDeploy target will exist. This means that if the target is declared more than once, the last one processed will be the definition that takes effect.

As an alternative to this, you can append as many target names to a dependency property as needed. For reusable build files the latter approach is the only method that ensures that a target name collision does not occur. As an example of extending the DeployDependsOn property, you could declare the DeployDependsOn target again, as shown here:

<PropertyGroup> CustomBeforeDeploy; $(DeployDependsOn); CustomAfterDeploy </PropertyGroup>
<Target Name="CustomBeforeDeploy">
  <!-- your steps here -->
</Target>
<Target Name="CustomAfterDeploy">
  <!-- your steps here -->
</Target>

In this example, the CustomBeforeDeploy and CustomAfterDeploy targets have been added to the list of targets to be executed. The targets shipped for Visual Basic .NET and C# projects declare empty targets like BeforeBuild as well as many dependency properties like RebuildDependsOn. You can find more information about both these topics at the MSBuild team blog.

Every target that can be called externally should have a complete list of targets in its DependsOnTargets attribute. This step is important because if a user wants to perform a specific task, let's say UnitTests, they should be able to specify that in the targets switch for msbuild.exe. By properly declaring dependent targets, your project file will support this.

Batching Tasks

One of the common questions about MSBuild is how to execute a loop. Because MSBuild uses a declarative language, you cannot specify an explicit loop. Instead, you must describe what you want the MSBuild engine to perform and let it handle the looping for you.

MSBuild uses a concept known as task batching, which allows a task to be executed once per each unique batch or group of values. Batches are always created based on item metadata. Take a look at a simple example using the contents of the Batching01.proj file, shown in Figure 2.

Figure 2 A Simple Batching Example

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
  <ItemGroup>
    <SampleItem Include="one">
      <Group>A1</Group>
      <Id>A363BE85-2CB1-4221-A9CB-2881B7699329</Id>
    </SampleItem>
    <SampleItem Include="two">
      <Group>A2</Group>
      <Id>48E171C8-2274-4567-84D5-D20C6B0CB363</Id>
    </SampleItem>
    <SampleItem Include="three">
      <Group>A2</Group>
      <Id>618E5BD8-650F-43c9-855E-259126284004</Id>
    </SampleItem>
    <SampleItem Include="four">
      <Group>A1</Group>
      <Id>65E8E8E7-5A3F-4e02-A1D9-34F797CB68D9</Id>
    </SampleItem>
    <SampleItem Include="five">
      <Group>A1</Group>
      <Id>43D0D1FE-304F-4aff-BE19-67AD2195872B</Id>
    </SampleItem>
  </ItemGroup>
  <Target Name="TaskBatching01">
    <!-- No batching here -->
    <Message Text="SampleItem: @(SampleItem)"/>
    <Message Text=" "/>
    <!-- Batches created based on Group Metadata -->
    <Message Text="SampleItem.Group: %(SampleItem.Group)"/>
    <Message Text=" "/>
    <!-- Batches created based on Id Metadata -->
    <Message Text="SampleItem.Id: %(SampleItem.Id)"/>
  </Target>
</Project>

This project file defines an item type named SampleItem, which includes five values. Each value has metadata values for Group and Id. The metadata for Id is unique for each item value, but Group has only two values, A1 and A2. Batching is triggered when an expression of the form %(ItemType.MetadataName) is encountered.

In the TaskBatching01 target, the Message task is invoked with <Message Text="SampleItem.Group: %(SampleItem.Group)"/>. This expression causes MSBuild to determine the unique batches defined by the Group metadata from the SampleItem item. Because I know that Group has two unique values, I know that two batches are created, one from values that have A1 as the Group metadata and the other from those with A2. Another Message task, <Message Text="SampleItem.Id: %(SampleItem.Id)"/>, creates batches based on the Id metadata value. The result of executing this target is shown in Figure 3.

 Figure 3 Batching01.proj Result

C:\Samples\Batching>msbuild Batching01.proj /t:TaskBatching01 /nologo 
 Build started 10/20/2008 1:27:58 AM. 
 Project "C:\Samples\Batching\Batching01.proj" on node 0 (TaskBatching01 target(s)). 
 SampleItem: one;two;three;four;five 
 SampleItem.Group: A1 
 SampleItem.Group: A2 
 SampleItem.Id: A363BE85-2CB1-4221-A9CB-2881B7699329 
 SampleItem.Id: 48E171C8-2274-4567-84D5-D20C6B0CB363 
 SampleItem.Id: 618E5BD8-650F-43c9-855E-259126284004 
 SampleItem.Id: 65E8E8E7-5A3F-4e02-A1D9-34F797CB68D9 
 SampleItem.Id: 43D0D1FE-304F-4aff-BE19-67AD2195872B 
 Done Building Project "C:\Samples\Batching\Batching01.proj" (TaskBatching01 target(s)). 
 Build succeeded. 
 0 Warning(s) 0 Error(s)

As you can see, the statement <Message Text="SampleItem: @(SampleItem)"/> flattens the SampleItem values into a single string passed into the Message task in place of the @(SampleItem) expression, resulting in "one;two;three;four;five." Contrast this with the behavior demonstrated with the <Message Text="SampleItem.Group: %(SampleItem.Group)"/> invocation. In this case, the Message task was executed twice. In the case of the SampleItem.Id instance, the Message task was executed five times.

One common use of batching is to recursively copy files from one location to another. In these cases you have to use the RecursiveDir well-known metadata to determine where the folder resides relative to the parent directory. (The set of MSBuild well-known metadata can be found in the "MSBuild Well-known Item Metadata" article on MSDN.) The project file Copy01.proj, shown in Figure 4, copies files from the src folder to the dest folder.

 Figure 4 Copy01.proj

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
  <ItemGroup>
    <!-- Get all files under src\ except svn files -->
    <SrcFiles Include="src\**\*" Exclude="**\.svn\**\*" />
  </ItemGroup>
  <PropertyGroup>
    <Dest>dest\</Dest>
  </PropertyGroup>
  <Target Name="CopyToDest">
    <Copy SourceFiles="@(SrcFiles)" DestinationFolder ="$(Dest)%(SrcFiles.RecursiveDir)" />
  </Target>
  <Target Name="Clean">
    <ItemGroup>
      <_FilesToDelete Include="$(Dest)**\*"/>
    </ItemGroup>
    <Delete Files="@(_FilesToDelete)"/>
  </Target>
</Project>

Inside the CopyToDest target, the Copy task is used along with batching to copy the files. Here are the contents of the src folder:

src 
¦ class1.cs 
¦ class2.cs 
¦ class3.cs 
¦ class4.cs 
¦ 
+---Admin 
¦ admin_class1.cs 
¦ admin_class2.cs 
¦ 
+---Utilities util_class1.cs util_class2.cs

In this example, the DestinationFolder is specified as $(Dest)%(SrcFiles.RecursiveDir). This statement breaks the SrcFiles item into three groups based on the value for RecursiveDir—an empty value for RecursiveDir, Admin, and Utilities. The Copy task is called three times, once for each group. During each of these calls only the files in the current group are passed to the Copy task as the SrcFiles item.

To see this clearly, take a look at the detailed log of the build shown in Figure 5. From the log, you can verify that the Copy task was executed three times, and each time only the files matching the current value for RecursiveDir were included in the SrcFiles item.

 Figure 5 Recursively Copying Files

Build started 10/26/2008 12:09:28 AM. 
 Project "C:\Samples\Batching\Copy01.proj" on node 0 (default targets). 
 Building with tools version "3.5". 
 Target "CopyToDest" in file "C:\Samples\Batching\Copy01.proj" from project "C:\Samples\Batching\Copy01.proj": 
 Using "Copy" task from assembly "Microsoft.Build.Tasks.v3.5, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a". 
 Task "Copy" Copying file from "src\Admin\admin_class1.cs" to "dest\Admin\admin_ class1.cs". 
 Command: copy /y "src\Admin\admin_class1.cs" "dest\Admin\admin_class1.cs" 
 Copying file from "src\Admin\admin_class2.cs" to "dest\Admin\admin_ class2.cs". 
 Command: copy /y "src\Admin\admin_class2.cs" "dest\Admin\admin_class2.cs" 
 Done executing task "Copy". Task "Copy" 
 Copying file from "src\class1.cs" to "dest\class1.cs". 
 Command: copy /y "src\class1.cs" "dest\class1.cs" 
 Copying file from "src\class2.cs" to "dest\class2.cs". 
 Command: copy /y "src\class2.cs" "dest\class2.cs" 
 Copying file from "src\class3.cs" to "dest\class3.cs". 
 Command: copy /y "src\class3.cs" "dest\class3.cs" 
 Copying file from "src\class4.cs" to "dest\class4.cs". 
 Command: copy /y "src\class4.cs" "dest\class4.cs" 
 Done executing task "Copy". Task "Copy" 
 Copying file from "src\Utilities\util_class1.cs" to "dest\Utilities\ util_class1.cs". 
 Command: copy /y "src\Utilities\util_class1.cs" "dest\Utilities\util_class1.cs" 
 Copying file from "src\Utilities\util_class2.cs" to "dest\Utilities\ util_class2.cs". 
 Command: copy /y "src\Utilities\util_class2.cs" "dest\Utilities\util_class2.cs" 
 Done executing task "Copy". 
 Done building target "CopyToDest" in project "Copy01.proj". 
 Done Building Project "C:\Samples\Batching\Copy01.proj" (default targets). 
 Build succeeded. 
 0 Warning(s) 0 Error(s)

This previous example is a common use of batching. Another is creating a .zip file. If you need your build process to create a .zip file, there are some tasks available that can assist you. One such task is the Zip task from the MSBuild Community Tasks project. This task is used in the Zip01.proj file shown in Figure 6.

Figure 6 Zip01.proj

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
  <PropertyGroup>
    <SrcFolder>src\</SrcFolder>
  </PropertyGroup>
  <ItemGroup>
    <SourceFiles Include="$(SrcFolder)*" Exclude="$(SrcFolder).svn\**\*"> </SourceFiles>
    <SourceFiles Include="$(SrcFolder)class1.cs;$(SrcFolder)class3.cs">
      <Access>public</Access>
    </SourceFiles>
    <SourceFiles Include="$(SrcFolder)class2.cs;$(SrcFolder)class4.cs">
      <Access>private</Access>
    </SourceFiles>
    <SourceFiles Include="$(SrcFolder)Utilities\*" Exclude="$(SrcFolder)Utilities\.svn\**\*">
      <Group>util</Group>
    </SourceFiles>
    <SourceFiles Include="$(SrcFolder)Admin\*" Exclude="$(SrcFolder)Admin\.svn\**\*">
      <Group>admin</Group>
      <Access>private</Access>
    </SourceFiles>
  </ItemGroup>
  <!-- Define default values here. -->
  <ItemDefinitionGroup>
    <SourceFiles>
      <!-- Default Group value -->
      <Group>common</Group>
      <!-- Default Access value -->
      <Access>public</Access>
    </SourceFiles>
  </ItemDefinitionGroup>
  <!-- Import SDC tasks from known location in source control -->
  <Import Project="..\Contrib\CommunityTasks\MSBuild.Community.Tasks.Targets"/>
  <Target Name="ZipFiles">
    <!-- Create an item with output in order to get the full path -->
    <PropertyGroup>
      <_ZipWorkingDir>output\</_ZipWorkingDir>
    </PropertyGroup>
    <MakeDir Directories="$(_ZipWorkingDir)"/>
    <!-- Zip the files based on what zip file they should be placed in. Also this task should be passed a full path for the WorkingDirectory. -->
    <Zip Files="@(SourceFiles)" 
         ZipFileName= "$(_ZipWorkingDir)%(SourceFiles.Group).%(SourceFiles.Access). zip"/>
  </Target>
</Project>

This project file declares an item list, SourceFiles, that contains the contents of the src folder. This item list also contains some additional metadata, Group and Access, which is used to determine which .zip file the files in the src folder should be placed in. The project uses the ItemDefinitionGroup element to define default values for Group and Access. All values in the SourceFiles item list that do not have a Group value explicitly defined use the value "common" and those that do not have a value for Access use the value "public".

The Zip task is used inside the ZipFile target, and the value $(_ZipWorkingDir)%(SourceFiles.Group).%(SourceFiles.Access).zip is provided for the ZipFileName property. In cases that include multiple batching expressions from the same item list, batches are created from unique combinations of the metadata used. For this statement, batches will be created from unique values for Group and Access. Because there are three values for Group and two for Access, there will be at most six batches. In this case there are only four batches consisting of the combinations shown in Figure 7.

fig07.gif

The other possible combinations (admin/public and util/private) never appear in the item so they will not be used. We now know that four .zip files should be created. If you execute the command msbuild Zip01.proj /t:ZipFiles, you'll see results shown in Figure 8.

 Figure 8 Creating Zip Files Based on Metadata

C:\Samples\Batching>msbuild Zip01.proj /nologo 
 Build started 11/3/2008 11:40:40 PM. 
 Project "C:\Samples\Batching\Zip01.proj" on node 0 (default targets). 
 Creating directory "output\". 
 Creating zip file "output\common.public.zip". added "src/class1.cs". added "src/class3.cs". 
 Created zip file "output\common.public.zip" successfully. 
 Creating zip file "output\common.private.zip". added "src/class2.cs". added "src/class4.cs". 
 Created zip file "output\common.private.zip" successfully. 
 Creating zip file "output\util.public.zip". added "src/Utilities/util_class1.cs". added "src/Utilities/util_class2.cs". 
 Created zip file "output\util.public.zip" successfully. 
 Creating zip file "output\admin.private.zip". added "src/Admin/admin_class1.cs". added "src/Admin/admin_class2.cs". 
 Created zip file "output\admin.private.zip" successfully. 
 Done Building Project "C:\Samples\Batching\Zip01.proj" (default targets). 
 Build succeeded. 
 0 Warning(s) 0 Error(s)

In this figure, you can see that the Zip task was called four times as expected. The name of each zip file contains both metadata values used for the batch.

The behavior described here is not limited to two metadata values or a single item list. You are free to use as many metadata values as necessary. The behavior for batching on more than one item list is different from what I've described and will not be discussed here.

Defining Dynamic Items and Properties

I've previously discussed items and properties, but I haven't really defined them. In MSBuild, items are ordered lists of objects with metadata, all of which share a particular item type, such as Resource or Compile. Properties are key-value pairs. Dynamic items and properties are those that you create inside a target., Static items and properties are declared outside a target.

In MSBuild 2.0 you had to use the tasks CreateItem and CreateProperty to create dynamic items and properties, respectively. In MSBuild 3.5, you can create them by using the same syntax as you use for static items. The preferred method is to use the ItemGroup and PropertyGroup elements inside a target.

Static values are always evaluated before any target executes. This means that every ItemGroup and PropertyGroup found outside a target must be expanded before a single build step can be executed. Property evaluation is quick, but item evaluation can be time-consuming, depending on the number of values an item contains. For this reason, if an item or property is only used with one target, it should be declared inside that target and not as a static value.

Property and item evaluation occurs in several passes. For static properties and items, properties are evaluated top to bottom (from the start of a file to the end of a file), including entering into any imported projects, and items are evaluated from top to bottom, also including entering into any imported projects. Dynamic properties and items are evaluated as they are executed.

When you create items and properties, the convention is to start the name for "internal" values with an underscore. Items and properties whose names do not start with an underscore indicate to users that they are free to override them. Given this convention, when you create those properties or items, you should first check to see whether they have already been defined with a condition. This is useful in case someone else has already defined the same property or item that should override yours.

To demonstrate this, take a look at the following declarations taken from Microsoft.Common.targets:

<PropertyGroup>
  <AssemblyName Condition=" '$(AssemblyName)'=='' ">$(MSBuildProjectName)</AssemblyName>
</PropertyGroup>

From this code, we can see that the property AssemblyName will be set to $(MSBuildProjectName) only if it has not already been previously declared.

MSBuild 3.5 includes new features related to dynamic items. For example, in MSBuild 2.0 you cannot modify the contents of an item list. After you include an item in an item list you cannot remove it. The workaround to this limitation is to create a new item excluding the value you are not interested in. MSBuild 3.5 now supports removing values from items through the Remove attribute. The project file RemoveItems01.proj, shown in Figure 9, demonstrates this.

 Figure 9 RemoveItems01.proj

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
  <ItemGroup>
    <SrcFiles Include="Batching\src\class1.cs"/>
    <SrcFiles Include="Batching\src\class2.cs"/>
    <SrcFiles Include="Batching\src\class3.cs"/>
  </ItemGroup>
  <Target Name="Remove">
    <Message Text="SrcFiles: @(SrcFiles)"/>
    <ItemGroup>
      <!-- Remove using value provided inside -->
      <SrcFiles Remove="Batching\src\class2.cs"/>
      <!-- Remove all items in SrcFiles that match the condition. In this case, a single file. -->
      <SrcFiles Remove="@(SrcFiles)" Condition="'%(Filename)%(Extension)'=='class3.cs'" />
    </ItemGroup>
    <Message Text="SrcFiles: @(SrcFiles)"/>
  </Target>
</Project>

There are two ways to use the Remove attribute on the ItemGroup. You can remove a value by passing the value that was included inside the Include attribute when the item was created. This approach is shown in the expression <SrcFiles Remove="Batching\src\class2.cs"/>. The other method shown in Figure 9 is to pass the entire item into the Remove attribute while placing a condition on the element.

Figure 10 shows the result of executing the Remove target. Based on the output shown in Figure 10, you can see that the class2.cs and class3.cs files were successfully removed from the SrcFiles item. You can also add and update metadata using the ItemGroup element inside of a target, using the same syntax you do for creating a dynamic item. To see how to update metadata values, take a look at the UpdateMetadata01.proj file shown in Figure 11.

 Figure 10 Removing Values from Items

C:\Samples>msbuild RemoveItems01.proj /t:Remove /nologo 
 Build started 10/26/2008 12:54:11 AM. 
 Project "C:\Samples\RemoveItems01.proj" on node 0 (Remove target(s)). 
 SrcFiles: Batching\src\class1.cs;Batching\src\class2.cs;Batching\src\class3.cs 
 SrcFiles: Batching\src\class1.cs 
 Done Building Project "C:\Samples\RemoveItems01.proj" (Remove target(s)). 
 Build succeeded. 
 0 Warning(s) 0 Error(s)

Figure 11 UpdateMetadata01.proj

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
  <ItemGroup>
    <Reference Include="IronPython, Version= ...">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>..\shared\IronPython-1.1\IronPython.dll</HintPath>
    </Reference>
    <Reference Include="log4net, Version= ...">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>..\binaries\log4net.dll</HintPath>
    </Reference>
    <Reference Include="nunit.core, ...">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>..\shared\nunit2.2\nunit.core.dll</HintPath>
    </Reference>
    <Reference Include="nunit.framework, Version= ...">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>..\shared\nunit2.2\nunit.framework.dll</HintPath>
    </Reference>
    <Reference Include="nunit.util, ...">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>..\shared\nunit2.2\nunit.util.dll</HintPath>
    </Reference>
  </ItemGroup>
  <Target Name="UpdateSpecificVersion">
    <Message Text="%(Reference.Identity) : %(Reference. SpecificVersion)"/>
    <Message Text=" "/>
    <!-- blank line -->
    <Message Text="Update Reference.SpecificVersion to True"/>
    <Message Text=" "/>
    <!-- blank line -->
    <!-- Update all Reference items to be SpecificVersion=True -->
    <ItemGroup>
      <Reference>
        <SpecificVersion>True</SpecificVersion>
      </Reference>
    </ItemGroup>
    <Message Text="%(Reference.Identity) : %(Reference.SpecificVersion)"/>
  </Target>
</Project>

This project file declares an item list named Reference. (In these examples, strong names are truncated to preserve space.) Also, the project contains a target named UpdateSpecificVersion. This target uses the ItemGroup element to update the SpecificVersion metadata for the Reference item list. The values for this metadata are printed before and after the update. Figure 12 shows the results.

Figure 12 Updating Existing Metadata

C:\Samples>msbuild UpdateMetadata01.proj /t:UpdateSpecificVersion /nologo 
 Build started 10/29/2008 12:07:39 AM. 
 Project "C:\Samples\UpdateMetadata01.proj" on node 0 (UpdateSpecificVersion target(s)). 
 IronPython, Version= ... : False log4net, Version= ... : False nunit.core, ... : False nunit.framework, Version= ... : False nunit.util, ... : False 
Update Reference.SpecificVersion to True 
 IronPython, Version= ... : True log4net, Version= ... : True nunit.core, ... : True nunit.framework, Version= ... : True nunit.util, ... : True 
 Done Building Project "C:\Samples\UpdateMetadata01.proj" (UpdateSpecificVersion target(s)). 
 Build succeeded. 
 0 Warning(s) 0 Error(s)

You can see an example of adding metadata in the AddMetadata01.proj file included with the article's sample code. I won't go into the details here. Along with modifying item list themselves, updating metadata and adding metadata to items while a target is executing is new functionality that has no equivalent in MSBuild 2.0.

Extending the Clean Process

This section relates to using MSBuild when you are building C# or Visual Basic .NET projects in Visual Studio. The behavior described here is contained within the target files used to build these types of projects and not within MSBuild itself.

When you modify the build process to create new files, you are responsible to extend the clean process to clean up those files. MSBuild cleans up files that it is responsible for automatically, but it cannot do so for files generated by custom targets. MSBuild maintains an item list named FileWrites that contains the files that need to be cleaned. This list is persisted to a file inside the obj folder that is referred to as the "clean cache." You can place additional values into the FileWrites item list so that they are removed when the project is cleaned up.

There are two drawbacks to doing this, however. Files to be deleted must reside under the output path, and you must append items to that list before the clean cache is written to disk, which occurs in either the Clean or IncrementalClean target, depending on the type of build that is executed. These targets will be called before the Build target completes.

In the sample code accompanying this article, you will find a Windows Forms project, WindowsFormsApplication1, in which I've defined the BeforeBuild target as follows:

<Target Name="BeforeBuild">
  <ItemGroup>
    <CustomConfigFile Include="$(OutputPath)settings.config" />
  </ItemGroup>
  <!-- Generate config file here -->
  <WriteLinesToFile File="@(CustomConfigFile)" Lines="config entries here" Overwrite="true" />
  <!-- Append to FileWrites so the file will be removed on clean -->
  <ItemGroup>
    <FileWrites Include="@(CustomConfigFile)"/>
  </ItemGroup>
</Target>

In this target, a file named, settings.config is created by using the WriteLinesToFile task. In this case the file contains only the text "config entries here," but it could contain values for the appSettings node as well as other content. Also, the FileWrites list is appended to, so the settings.config file will be removed when a cleanup is executed.

In Figure 13 you can see the result of executing the Build target and then the Clean target (some lines that are not relevant are excluded). You can also see that settings.config was successfully deleted when the Clean target was executed. If the customizations were placed in the AfterBuild target instead of the BeforeBuild target, the file would not have been removed on Clean. To accomplish this, you have to extend the clean process.

 Figure 13 Clean Target Results

C:\Samples\WindowsFormsApplication1>msbuild WindowsFormsApplication1.csproj /fl /t:Build;Clean /nologo ... 
 CoreClean: Deleting file "C:\Samples\WindowsFormsApplication1\bin\Debug\settings.config". 
 Deleting file "C:\Samples\WindowsFormsApplication1\bin\Debug\WindowsFormsApplication1.exe.config". 
 Deleting file "C:\Samples\WindowsFormsApplication1\bin\Debug\WindowsFormsApplication1.exe". 
 Deleting file "C:\Samples\WindowsFormsApplication1\bin\Debug\WindowsFormsApplication1.pdb". 
 Deleting file "C:\Samples\WindowsFormsApplication1\obj\Debug\WindowsFormsApplication1.Form1.resources". 
 Deleting file "C:\Samples\WindowsFormsApplication1\obj\Debug\WindowsFormsApplication1.Properties.Resources.resources". 
 Deleting file "C:\Samples\WindowsFormsApplication1\obj\Debug\WindowsFormsApplication1.csproj.GenerateResource.Cache". 
 Deleting file "C:\Samples\WindowsFormsApplication1\obj\Debug\WindowsFormsApplication1.exe". 
 Deleting file "C:\Samples\WindowsFormsApplication1\obj\Debug\WindowsFormsApplication1.pdb". 
 Done Building Project "C:\Samples\WindowsFormsApplication1\WindowsFormsApplication1.csproj" (Build;Clean target(s)). 
 Build succeeded. 
 0 Warning(s) 0 Error(s)

There are two targets that can be overridden in your project file that are executed when the Clean target is called. Those targets are BeforeClean and AfterClean. If you define either of these targets in the project file, the target will be called at the appropriate time. These targets must be defined after the Import statement for either Microsoft.CSharp.targets or Microsoft.VisualBasic.targets.

Another approach is to use target dependencies. You can extend the CleanDependsOn property to inject your own target into the cleanup process. Take a look at the customizations to the WindowsFormsApplication2 project shown in Figure 14, which occur after the Import statement for Microsoft.CSharp.targets.

Figure 14 WindowFormsApplication2

<Target Name="AfterClean">
  <Message Text="AfterClean target executed"/>
</Target>
<!-- Inject a custom target into Clean by extending CleanDependsOn -->
<PropertyGroup>
  <CleanDependsOn> CustomBeforeClean; $(CleanDependsOn); CustomAfterClean </CleanDependsOn>
</PropertyGroup>
<Target Name="CustomBeforeClean">
  <Message Text="CustomBeforeClean target executed"/>
</Target>
<Target Name="CustomAfterClean">
  <Message Text="CustomAfterClean target executed"/>
</Target>

In this listing, the AfterClean target is overridden and two targets are placed in the CleanDependsOn property. These targets only display a message to demonstrate that the target was executed, but in an actual build process they could clean up resources. Figure 15 shows the results of calling the Clean target (some lines were shortened to preserve space). As expected, the targets were called at the appropriate times.

Figure 15 Clean Target Results

C:\Samples\WindowsFormsApplication2>msbuild WindowsFormsApplication2.csproj /t:Clean /nologo 
 Build started 10/26/2008 1:17:40 PM. 
 Project "C:\Samples\WindowsFormsApplication2\WindowsFormsApplication2.csproj" on node 0 (Clean target(s)). 
 CustomBeforeClean target executed 
 CoreClean: 
 Deleting file "\bin\Debug\WindowsFormsApplication2.exe". 
 Deleting file "\bin\Debug\WindowsFormsApplication2.pdb". 
 Deleting file "\obj\Debug\WindowsFormsApplication2.Form1.resources". 
 Deleting file "\obj\Debug\WindowsFormsApplication2.Properties.Resources.resources". 
 Deleting file "\obj\Debug\WindowsFormsApplication2.csproj.GenerateResource.Cache". 
 Deleting file "\obj\Debug\WindowsFormsApplication2.exe". 
 Deleting file " \obj\Debug\WindowsFormsApplication2.pdb". 
 AfterClean: AfterClean target executed 
 CustomAfterClean: 
 CustomAfterClean target executed 
 Done Building Project "C:\Samples\WindowsFormsApplication2\WindowsFormsApplication2.csproj" (Clean target(s)). 
 Build succeeded. 
 0 Warning(s) 0 Error(s)

Organizing Targets

As a build process grows, you need to organize where the different pieces reside. Generally, it is better to use files with dedicated responsibilities and whose contents reflect their purpose. To address this situation when you are defining a build process for a product, it is helpful to create three files:

  • MyProduct.settings.targets
  • MyProduct.targets
  • MyProduct.csproj

You can use MyProduct.setting.targets to contain various properties related to shared utilities used during the build and deployment processes as well as any other common settings. For example, if you are using NUnit, a setting that you might place in this file is the location of the NUnit references. When properties are declared in this file, they should always be declared so that they will not override any existing value. This file contains the default settings. (This is the same situation as discussed in the section "Defining Dynamic Items and Properties.")

MyProduct.targets contains the UsingTask elements that declare the custom tasks that are being used as well as any shared targets. This file defines how the product gets built in the sense that most of the targets as well as their dependencies should be contained in this file.

MyProduct.csproj defines what gets built. This project file declares all the items and properties that the MyProduct.targets file uses within its targets. The contract between these two files is represented by these shared items and properties. This file should also import MyProduct.settings.targets at the top and MyProduct.targets file at the bottom.

By doing so you can override any values in MyProduct.settings.targets and prepare properties and items for MyProduct.targets. If you relate this build process to that for a C# project, MyProduct.targets is equivalent to Microsoft.CSharp.targets (and Microsoft.Common.targets), and the other two files form the actual project file.

Using Wildcards

Visual Studio project files do not use wildcards to populate items that Visual Studio will be interacting with. For example, you could define the Compile item, which contains a list of files that will be sent to the compiler, as follows:

<ItemGroup>
  <Compile Include="src\*"/>
</ItemGroup>

This expression would place all the files in the src folder into the Compile item, and while this is valid and will work, it is not the best approach to take outside casual projects. Any edit made to the project file by Visual Studio will cause this item to expand to list each file individually. Also, this approach makes it easy to forget to check in to source control new or removed files from your local machine. Instead of using wildcards for items maintained by Visual Studio, you should explicitly include each file.

In this article I'll taken a look at a few key recommendations that you can use to create better build process for your products. As with all best practices, there will be situations in which these rules may not apply one hundred percent of the time and need to be bent a little. The best way to learn which of these practices works for you is to employ them and see which ones work and which ones fail. I would be honored to hear your stories, good and bad, about these best practices. You can contact me at sayed.hashimi@gmail.com.

I would like to thank Dan Moseley, from the MSBuild team, and Brian Kretzler for their invaluable help on this article.


Sayed Ibrahim Hashimi has been working with MSBuild since the early preview bits of Visual Studio 2005 were released. He is the author of Inside the Microsoft Build Engine: Using MSBuild and Team Foundation Build (Microsoft Press, 2009), the coauthor of Deploying .NET Applications: Learning MSBuild and ClickOnce (Apress, 2006), and has written for several publications. He works in Jacksonville, Florida, as a consultant and trainer, with expertise in the financial, education, and collection industries. You can reach Sayed at his blog, sedodream.com.