Basic concepts and principles

Team Build is a build framework constructed upon MSBuild and MSTest and integrated with Visual Studio and Team Foundation Server (TFS). At a minimum, a Team Build build consists of:

  • a build agent
  • a build definition,
  • a Team Foundation Studio project file called TFSBuild.proj
  • and the TeamFoundation.Build.targets file.
Working effectively with Team Build requires a good understanding of each of these. The first three are relatively straight-forward. The fun really begins as we start to work the targets file.

The Build Agent

A build agent is a service installed and running on a Windows computer that executes builds on request. In Visual Studio we can create a list of these build agents, giving them each a name and providing connection details such as the computer name, port and protocol. We pick the desired build agent from this list in Visual Studio when defining or invoking a Build Definition. Figure 1 shows the Visual Studio 2008 dialog for registering a build agent.

Registering a Build Agent with Visual Studio 2008
Figure 1: Registering a Build Agent with Visual Studio 2008

The Build Definition

A build definition defines the when, where and what of a particular build execution. They are defined in Visual Studio 2008 using a six-page dialog. Figure 2 shows that each build definition needs a name and may have a description. In addition, the definition can be enabled or disabled via a checkbox. Disabling a build definition simply prevents it from being used to kick-off a build.

Figure 2: Setting the name and description of a build description

A build definition defines a TFS workspace. For those not familiar with them, a TFS workspace is simply a mapping between TFS Source Control folders and working directories. The workspace defined in the build definition is passed to the build agent when the build definition is used to execute a build. The agent uses the workspace definition to determine which files to copy from TFS Source Control and into which working directories to copy them. Figure 3 shows the workspace definition page in the Build Definition dialog.

Figure 3: Configuring the workspace for the build definition

In addition to the workspace, the build definition defines a number of other settings. These settings include:

  • the source control directory in which the relevent TFSBuild.proj file is located (Figure 4),
  • what build results to keep (Figure 5),
  • the build agent to use (Figure 6),
  • and when the build will run (Figure 7).
As well as all this, when you exceute a build definition interactively, you can overide a number of the settings and specify some extra property values to be passed to the build engine.

Figure 4: Setting the location of the project file to build

Figure 5: Setting what build results to keep

Figure 6: Setting the default Build Agent for the build and default output location for the build results

Figure 7: Setting when the build is run

Build Definition Strategy

Conceptually, build defintions are a great idea. Unfortunately, their implementation misses a number of opportunies.

Most modern small team projects want to run incremental builds whenever a new set of files are checked into source control, and a thorough, full build each night or at the end of each week. These two builds are likely to differ only in the matter of a few property values that control whether the build is incremental or full and whether or not to execute certain optional targets. Ideally, the team would define two build definitions, one with a check-in trigger and one with a daily trigger, both pointing at the same build project file. The defintions would store and pass to the build agent the relevant property values that indicate whether the build is to be incremental or not.

Unfortunately, property values can only be specified when a build is queued for execution interactively. Automatically triggered build definition have nowhere to specify build property values.

Another way to achieve reuse of the same build project file for differnt builds would be to specify a different starting target in the build definition. This approach does not require build definitions to use property values to differentiate between the two builds.

Unfortunately, build definitions always invoke the EndToEndIteration target. Build definitions have nowehere to specify an alternative target to excute.

The obvious solution is to create two build project files in the same source control folder, factoring out the stuff that is common between the two into a third file that the other two reference. The two project files only need to set the handful of relevant properties to the values that cause the correct build to be executed. Everything else can go in the third file that both the others import.

Unfortunately, a build definition only specifies the source folder in which the project file lives. The project file must be called TFSBuild.proj. Therefore, the two project files in the same directory is not possible.

The only solution is two build definitons, each pointing to a different source control folder containing slightly varying project files. The common stuff can still be factored out into a separate file but it now requires a place to live. It becomes easier to simply duplicate the project file and tweak a few properties. This might be ok for a fairly standard build but for a highly customised build, it could rapidly lead to a maintenance headache and possibly unexpected build failures when one project file is updated and the other is not.

The possibility of build scripts varying leads us to another consideration. Build defintions always invoke the latest version of a build project file and related scripts. During the life of a project, the build scripts could change significantly. Without the facility to refer to a labeled version of a build script or the version from a specific date, it becomes difficult to rerun an old build using the versions of the build scripts that it was originally built with.

Given these short-comings, the implementation of build definitions seems at best to indicate old-fashioned, structured design thinking, rather than a more object or component-oriented thought process; low-level design is driven more by the question, "How do we execute a build process?", than by the question, "How do we get a project to build itself?". The focus is on executing a function rather than adding intelligent behaviour to a project object or component. Alternatively, these poor low-level design decisions could simply indicate a lack of experience with or knowledge of typical build scenerios on the part of the developers.

Nevertheless, however frustrating, these limitiations are not show-stoppers. Work arounds are relatively straight-forward albeit irritating to have to use.

The Build Project File

The TFSBuild.proj sets the values of properties and items used by the targets in the TeamFoundation.Build.targets file. The targets file is completely generic and has no content that is specific to any particular project. The TFSBuild.proj file compliments the targets file by supplying all the values specific to the project such as solutions to build, the sets of tests to execute, and a myriad of flags to control the behavior of the build.

The Build Targets File

The targets file is typically located within the MSBuild installation in the <Drive>:\Program Files\MSBuild\Microsoft\VisualStudio\TeamBuild directory and forms the heart of the framework. At a very high level, the target file provides a set of targets to do the following:

  1. Ensure some required properties are defined (CheckSettingsForEndToEndIteration)
  2. retrieve a set of default proeprty values from TFS (InitializeBuildProperties)
  3. insert a record the build into the TFS build database (InitializeEndToEndIteration)
  4. delete and recreate the build defintion's workspace (InitializeWorkspace)
  5. delete either everything from the last build ro just the output (Clean)
  6. create working directories in which to put source code files (InitializeBuild)
  7. retrieve all the source code files from the source control folders specified in the build definitions's workspace (Get)
  8. apply the build label to all the source files in the source control folders of the build definitions's workspace (Label)
  9. build all the individual projects within the list of solutions specified in the project builld file (Compile)
  10. associate all the change sets involved with the build and update related work items (GetChangesetsAndUpdateWorkItems)
  11. execute all the tests in the list of test containers or test meta-data files supplied in the build project file (Test)
  12. copy the build output to its final resting place (DropBuild)

Comparing this to my theoretical list of build steps in Adventures in Software Builds, this looks like a reasonable foundation to build upon. Nevertheless, the lists have a few immediately obvious differences. Team Build looks like it does a considerable amount of initialization work, the first four steps and step six are all initialization steps. It also looks like Team Build provides little post-build activity such as api document generation, success/failure notification, and static analysis. The most significant difference, however, is the apparent lack of iteration of a subset of the build ssteps for each component; equivalents for most of the steps from my component build list can be found in the flat list above.

To understand these differences better we need to expand upon the Team Build target list above. That list, in fact, is a very big simplification. Some of the steps above are actually grouped together into enclosing targets and others represent half a dozen sub-targets. More accurately, the flow of the build is as follows:

  • Check Settings for End to End Iteration
  • Initialize Build Properties
  • Initialize End to End Iteration
  • Initialize Workspace
  • Team Build
    • If a 'from scratch' build is required then
      • Clean All
    • If an 'incremental' build is required then
      • Clean Compilation Output
        • For each solution in each configuration
            • Clean Solution
    • Initialize Build
    • Prebuild
      • Get
      • Label
    • Compile
      • For each solution in each configuration
        • Compile Solution
    • Post Build
      • Get Change Sets and UpdateWork Items
  • Test
    • For each configuration
      • If using test containers
        • Execute all tests in the test containers
      • If using test lists
        • Exceute the tests in each test list
  • Drop Build
Note: In preparing the Team Build target lists in this article, I was helped by Martin Danner's target map and another prepared in-house by a colleague.

However, this expanded list is still not the end of the story because it ignores targets supplied specifically to be overridden to extend or modify the default behaviour of the build. Most of the targets in the second list have an empty BeforeX and empty AfterX target that exist purely to enable extra targets to be inserted into the above sequence.

Comparing this expanded list with my theoretical list of build steps earlier, further highlights the most signifcant difference between that and the Team Build target framework; the lack of iteration through a subset of steps for each component. Logical layers or components are generally organized as Visual Studio solutions. Therefore, the TeamBuild target in the expanded target list above looks like the equivalent of my component-build step. Unlike in my theoretical list of steps, the TeamBuild target does not execute repeatedly for each layer or component. Instead targets within the TeamBuild target do so; in other words the scope of the iteration is very different. The clean targets are executed for every component or layer. Then the source code for all the components is retrieved and labeled (get and label), and then each component or layer is compiled in turn. Tests are only run after all the components have been compiled. Instead of building and testing component by component, Team Build applies each major step (clean, compile, test, etc) in the build in turn to all of the components.

This system-wide approach is less intuitive than the component by component approach. It means that:

  • inter-component dependencies must be considered and respected at each major step,
  • instead of finding out quickly if a component builds and tests ok, all components have to be compiled before the testing of the first component begins
  • if building component by component and a test fials in one fo the first components built we know about very soon after ti happens. In contrast, running all the tests after building every component we have to wait for all the tests to execute before we know if any of the tests have failed.
  • poorly written tasks performed system-wide can suck up far more resources, especially memory

The solution is simple, run the build process multiple times for each component using the same build number and label. Unfortunately, this is not as easy as it seems. Firstly, one of those intialize targets, interacts with a datbase of builds within TFS. This does not like different builds to have the same build number. Secondly, there is no easy way to chain Build Definitions together to force one to run after the other. It is defiantely possible to do so, but for now I have found it too difficult to bend the framework to fit my preferred component by component approach. This is something I would normally revisit in detail at some point in the future but given the significant changes in build technology for Visual Studio 2010, the investment in doing so is probably no longer worthwhile.

Overriding and Extending the Team Build Targets

I do not want to suggest that the Team Build framework is completely inflexible. The Team Build targets above are only a starting point. A large number of additional targets in the file exist only to act as extension points. They are intended to be overriden to perform specific behaviour not covered 'out of the box'. These targets with suggested reasons for overriding include:

  • Before End To End Iteration (BeforeEndToEndIteration) - override to check or set initial propertiy values
  • Build Number Override Target (BuildNumberOverrideTarget) - override to use a different build numbering scheme
  • After End To End Iteration (AfterEndToEndIteration) - override to complete any specific set up work required before the start of the build
  • Before Initialize Workspace (BeforeInitializeWorkspace)
  • After Initialize Workspace (AfterInitializeWorkspace)
  • Before Clean (BeforeClean)
  • Before Clean Configuration (BeforeCleanConfiguration)
  • Before Clean Solution (BeforeCleanSolution)
  • After Clean Solution (AfterCleanSolution) - override to check and set any solution-specific resources to their initial state
  • After Clean Configuration (AfterCleanConfiguration) - override to check and set any configuration-specific resources to their initial state
  • After Clean (AfterClean) - override to check and set any other specific resources to their initial state
  • Before Get (BeforeGet)
  • After Get (AfterGet) - override to retrieve any further information from the TFS Source Control repository
  • Before Label (BeforeLabel) - override to modify the property values that define the string used to label the build
  • After Label (AfterLabel) - override to lable up additional items in the TFS Source Control repository
  • Before Compile (BeforeCompile)
  • Before Compile Configuration (BeforeCompileConfiguration)
  • Before Compile Solution (BeforeCompileSolution)
  • After Compile Solution (AfterCompileSolution)  - override to execute additional steps specific to the type of project
  • After Compile Configuration (AfterCompileConfiguration)
  • After Compile (AfterCompile) - override to create database schema generation and data population scripts
  • Before Get Changesets And Update WorkItems (BeforeGetChangesetsAndUpdateWorkItems)
  • After Get Changesets And Update WorkItems (AfterGetChangesetsAndUpdateWorkItems)
  • Before Test (BeforeTest) - override to setup the database and other types of server required to run all the tests
  • Before Test Configuration(BeforeTestConfiguration)
  • After Test Configuration(AfterTestConfiguration)
  • After Test (AfterTest)
  • Generate Documentation (GenerateDocumentation) - override to:
    • generate and publish API documentation using Sandcastle, Doxygen, or equivalent.
    • generate and publish static analysis reports using FXCop, StyleCop or equivalent.
    • generate and publish a  'bill of materials' for this build containing lists of new features implemented, defects resolved and left outstanding, etc.
  • Package Binaries (PackageBinaries) - override to package assemblies into MSI files.
  • Before Drop Build (BeforeDropBuild)
  • After Drop Build (AfterDropBuild) - override to notify individuals by e-mail that the build completed
  • Before On Build Break (BeforeOnBuildBreak)
  • After On Build Break (AfterOnBuildBreak) - override to notify individuals by e-mail that the build failed
  • BeforeGetChangesetsOnBuildBreak (BeforeGetChangesetsOnBuildBreak)
  • AfterGetChangesetsOnBuildBreak (AfterGetChangesetsOnBuildBreak)
  • BeforeCreateWorkItem (BeforeCreateWorkItem)
  • AfterCreateWorkItem (AfterCreateWorkItem)

To override any of these, you create a target with the same name either inside the TFSBuild.project file after the import of the Team.Foundation.Build.targets file, or in a separate targets file that the TFSBuild.project file imports after the Team.Foundation.Build.targets file.

As you might expect several teams have identified common extensions that they need, and many of these have made their way into an open-source-style extensions package consisting of additional tasks and targets that can be incorporated into the build. This includes support for working with assembly files, BizTalk, SQL Server, de-tokenizing configuration files, etc. On my project we already use the de-tokenizing, assemblty and code-quality support from this package.

It looks like there is sufficient room to override the build wherever you need to. On my project we have, so far, overidden the following:

  • Build Number Override Target
  • After Get Target
  • After compile Solution target
  • Before Test Target

We have, however, also had to override the CoreTestConfiguration targets described in the Test Target section. This highlights another potential problem with the framework; there is no way to insert behaviour between the processsing of items when a collection is passed as a parameter to a task.

Another potential problem is the omission of override targets between some of the main steps. The before and after targets are part of compound targets. If there is something that needs to happen between finishing compiling and running tests, it has to be placed in either the afterCompile or beforeTest target. Neither of these may actually be truly suitable places for that function because switching off a compound target such as test (using the property $(RunTests) for example), switches off the related before and after targets too.

Fortunately, there is a way to insert a new target between two existing targets. the dependency list for each target is specified in a suitably named property. Overriding that property, and inserting the name of an additional target into the list works very nicely. In fact, it works so nicely, that it really makes the provision of the before and after targets redundant. They could be removed from the targets file simplifying it considerably. To do add an 'after' target simply override the relevent property defining the list of dependencies for the core target and add the name of the additional after target.

The result is that my project's TFSBuild.project files import three other target files, the standard Team.Foundation.Build.targets file, the targets file from the extensions package, and a file that contains organization-specific targets and tasks we want available to all the TFSBuild.project files in our organization.

Desktop builds

One of our original requirements for the build required developers to be able to run the compile and test steps of the build on their workstations.  Team Build provides for this situation. Two targets, DesktopBuild and DesktopRebuild provide this. Throughout the file, the targets and proeprties are guarded by conditions testing the value of the IsDesktopBuild property. Setting this property to true and executing the TFSBuild.proj file with either of these as the starting target, runs the relevent subset of targets.

Next, read Microsoft Team Build Details

Follow me on Twitter...