Welcome!

.NET Authors: Bruce Armstrong, Pat Romanski, Liz McMillan, Yeshim Deniz, Dmitry Sotnikov

Related Topics: .NET

.NET: Article

Refactoring Your MSBuild Scripts

How to make maintainable processes

Some things change how you fundamentally program. Automation is one of those things. It is what will save you from wasting your weekend stepping through tedious and error-prone processes like regression testing (unit, integration, performance, functional, etc.), builds, deployment, or even documentation. Automation is one of those buzzwords we all know our projects should have (like "performance," "security," "maintainability," etc...), but the question is how?

Microsoft gave us a huge answer with .NET 2.0's MSBuild - its new build platform with a process-oriented scripting language. It can revolutionize not just how you build, but also how you test, deploy, and automate many of your development processes. The problem with these scripts, like the problem with anything else, is that they must be maintained. Perhaps the best way to maintain them is to keep them refactored - something that MSBuild makes very easy.

MSBuild Background
Refactoring is improving the code (usually by eliminating redundancy) while keeping the same functionality. To eliminate redundancy, you have to be able to split out code into separate, reusable chunks, and then pass data between those chunks. A simple MSBuild script is just a series of tasks. Therefore refactoring requires these steps:

  1. Pull out reusable properties.
  2. Group multiple tasks into a single target.
  3. Split out these targets into physically separate files
  4. Call these targets as needed.
  5. Pass data to and from these targets as we call them.
The first two steps are trivial. For example, this snippet shows two tasks (Message and Copy) grouped into a single target with variables named Var1 and Var2. Note that you can use one variable in defining another.

<PropertyGroup>
    <Var1>abc</Var1>
    <Var2>$(abc)def</Var2>
    <root>C:\myProj</root>
</PropertyGroup>

<Target Name="EndPoint">
    <Message Text="This is a task, print $(Var2)" />
    <Copy SourceFiles="$(root)\abc.txt"
       DestinationFolder="$(root)\myFolder" />
</Target>

The rest of this article will focus on the remaining three steps.

Splitting Out Scripts
Each MSBuild script is called a "project file." The extensions doesn't really matter, but will often be *.msbuild, *.task, or *.proj. Each project file is enclosed in the <Project> node (to save space, we'll omit the project node in the code snippets). MSBuild provides two ways to reach across physically separate projects files: (1) the Import Element and (2) the MSBuild task (not to be confused with MSBuild the engine itself). (See Table 1)

The Import element lets you insert one project file into another. For example, suppose you had a bunch of system-wide variables like application install paths. Getting a new version of an application may require updating its path, so you want to store all paths in a single file so it can be updated just once. You could do this by putting those paths in their own project (like "CommonProperties.proj"), and then importing that project into any script that needed it. This lets you update variables without touching each script.

CommonProperties.proj
   <PropertyGroup>
      <NameA>Value1</NameA>
      <NameB>Value2</NameB>
   </PropertyGroup>

Some other script:
   <Import Project="CommonProperties.proj" />
   <PropertyGroup>
      <NameB>Overrided</NameB>
      <NameC>Value3</NameC>
   </PropertyGroup>

   <Target Name="EndPoint">
      <Message Text="value from Include: $(NameA)" />
      <Message Text="value from Include: $(NameB)" />
   </Target>

Note that the properties in the imported script get overridden by any properties of the same name in the parent script.

Besides importing an entire project file, you can split your targets into separate project files and call them with the MSBuild task. The purpose of the MSBuild task is to call other targets, so we'll explain it more in our next section.

Ways to Call Targets
Once we have separate files, we have to be able to call targets in those files. MSBuild provides at least three ways to do this: (1) The MSBuild task that we just talked about, (2) The CallTarget task, and (3) the DependsOnTargets attribute of a target. The MSBuild task is a powerful way to call targets. (See Table 2) Because Visual Studio 2005 csproj projects are actually just MSBuild scripts, most developers are already using the MSBuild task. For example, these lines would build the MyApp project:

   <MSBuild Projects="$(rootdir)\MyApp.csproj"
      Targets="rebuild" Properties="Configuration=release" />

This task has three main attributes: the physical Project(s) file, the Target(s) to call within that project, and finally any Property(or Properties) to pass to that target. The power of the MSBuild task is that you can call any MSBuild project file and pass in your own properties (we'll discuss this more in the next section). This essentially lets you abstract out your targets to separate files as if they were class libraries.

When you don't need the full power of the MSBuild task, you can use the CallTarget task. Unlike the MSBuild task, it requires that the target be in the current project file (including all imported files), and it doesn't let you pass in your own property values. For example, you may have a build script that calls certain targets in a specific order:

<CallTarget Targets="GetSource" />
<CallTarget Targets="Compile" />
<CallTarget Targets="UnitTest" />

Yet another way to call targets is by using the "DependsOnTargets" attribute to specify which other targets the current target depends on. MSBuild is smart enough to automatically order the targets by their dependencies.

<Target Name="GetSource">
</Target>
<Target Name="Compile" DependsOnTargets="GetSource">
</Target>
<Target Name="UnitTest" DependsOnTargets="Compile">
</Target>

MSBuild is also smart enough to call each dependency only once. So if we added the GetSource target to the last line, so that it appears twice, it would still be called just once:

<Target Name="UnitTest" DependsOnTargets="Compile;GetSource">

Note that using CallTarget is an explicit approach - you start at the first target and march forward, explicitly calling each target in the order you want. On the contrary, DependsOnTargets is an implicit approach - MSBuild starts at the end of your script and steps backwards to infer the calling order.


More Stories By Timothy Stall

Tim Stall is a software developer at Paylocity, an independent provider of payroll and human resource solutions. He can be contacted at tims@paylocity.com.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.