Condividi tramite


Build Extensibility with .NET Framework 4

Introduction

With the release of .NET Framework 4 and Visual Studio 2010 comes MSBuild 4.0. Among the many great features in this version are new mechanisms to allow you (or your build lab) to extend the default build targets files with your own customizations. While some of this functionality did exist in previous versions, it has been made even easier and more powerful. This article will help you understand the new extensibility mechanisms and how they can be most effectively used. By way of example, we will show how to add entry points to the build system where you could perform source control operations and custom publishing of your outputs.

The Old Way

With versions of the .NET Framework prior to 4.0, the provided targets files included a variety of special targets which could be overridden to allow the running of additional targets. Let's look at the 'Build' target, located in the Microsoft.Common.targets file in your .NET Framework directory. That target declares several other targets as dependencies (i.e. targets which are required to run before it may execute.)

     <PropertyGroup>
        <BuildDependsOn>
            BeforeBuild;
            CoreBuild;
            AfterBuild
        </BuildDependsOn>
    </PropertyGroup>
    <Target
        Name="Build"
        Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
        DependsOnTargets="$(BuildDependsOn)"
        Outputs="$(TargetPath)"/>

The 'Build' target declares its DependsOnTargets as coming from the $(BuildDependsOn) property group, which includes 'BeforeBuild', 'CoreBuild' and 'AfterBuild'. Thus, when the 'Build' target is invoked, MSBuild will ensure that the 'BeforeBuild', 'CoreBuild' and 'AfterBuild' targets are executed before any tasks are processed within the Build target. MSBuild will also guarantee that they the run in the order specified. If any of them had already run previously (and not skipped), those entries will be skipped. Thus, if 'CoreBuild' had already been executed by the time 'Build' was invoked, then 'BeforeBuild' and 'AfterBuild' would be invoked, in order, prior to executing the first task in 'Build'.

By default, the 'BeforeBuild' and 'AfterBuild' targets are empty. They serve as placeholders for user-defined targets in your own project files. The position of those targets is built into the default Microsoft.Common.targets (and other targets files with similar designs.) In this case, you can override 'BeforeBuild' or 'AfterBuild' in your project file, and when 'Build' is invoked, your version of 'BeforeBuild' or 'AfterBuild' will be invoked according to the rules above, providing your version of those targets is encountered after the original targets when the project file is parsed. This is based on the "last target definition wins" rule for targets in MSBuild – the last target that MSBuild parses with a given name will be the one which gets used during the build.

In our hypothetical source-control and publishing example, we might define our new targets as follows:

     <Import Project="Microsoft.Common.targets" />  <-- the original BeforeBuild and AfterBuild are defined here
    <Target Name="BeforeBuild">
        <Message Text="Performing source control operations" />
        ... execute tasks to do source control ...
    </Target>
    <Target Name="AfterBuild">
        <Message Text="Copying outputs for publishing." />
        ... execute tasks move your output files as appropriate ...
    </Target>

If you were to then build your project and specify a target of 'Build', you would see your source control operations occur first. Once the build was completed, you would see your new copying tasks execute.

Note there is a bit of naming confusion in looking at these targets. As you can see above, the 'Build' target doesn't really do anything other than specifyDependsOnTargets and a condition. Its whole purpose is to give you the ability to specify targets to run before and after 'CoreBuild', which does the real work. This works fine as far as it goes, but it suffers from a few drawbacks:

  • The build system must have been authored with the extensibility points in place – in the example above, if 'Build' had not been authored to depend on 'BeforeBuild', there would have been no way to extend the system.
  • You must implement the extensibility using a level of indirection (like the 'Build'/'CoreBuild' system above) if you intend to support an extensibility point afterthe target to be extended.
  • Your own extensions cannot themselves be extended without doing the above.

In MSBuild 4.0, all of these caveats and restrictions have been eliminated.

The New Way: BeforeTargets and AfterTargets

We introduce two new attributes to the Target element in MSBuild. Consider a target 'X' with these attributes:

  • BeforeTargets – Specifies a list of targets before which 'X' should run.
  • AfterTargets –Specifies a list of targets after which 'X' should run.

These attributes both behave in exactly the opposite fashion from DependsOnTargets, in that another target's execution will trigger this target to execute, rather than this target's execution triggering another target. A simple example will illustrate how this works:

     <Import Project="Microsoft.Common.targets" />
    <Target Name="GetSourceFiles" BeforeTargets="Build">
        <Message Text="GetSourceFiles now executing" />
        ... execute your source control operations ...
    </Target>
    <Target Name="CopyOutputsForPublishing" AfterTargets="Build">
        <Message Text="CopyOutputsForPublishing now executing" />
        ... execute your copying operations ...
    </Target>

These attributes may not have been named as well as they could have been. Consider reading them as "RunBeforeTargets" and "RunAfterTargets". If you execute the 'Build' target, the 'GetSourceFiles' target will be executed first (that is, 'GetSourceFiles' runs before target 'Build'), and then after 'Build' is finished, the 'CopyOutputsForPublishing' target will execute ('CopyOutputsForPublishing' runs after target 'Build'). This mechanism can be used to extend any targets, including your own extensions. The following, for example, would be valid (and behave as expected):

     <Import Project="Microsoft.Common.targets" />
    <Target Name="GetSourceFiles" BeforeTargets="Build">
        <Message Text="GetSourceFiles now executing" />
    </Target>
    <Target Name="CopyOutputsForPublishing" AfterTargets="Build">
        <Message Text="CopyOutputsForPublishing now executing" />
    </Target>
    <Target Name="NotifyBuildIsStarting" BeforeTargets="GetSourceFiles">
        <Message Text="NotifyBuildIsStarting now executing" />
    </Target>

Here your 'NotifyBuildIsStarting' target will execute, then 'GetSourceFiles', then 'Build', then 'CopyOutputsForPublishing'. Other combinations you might imagine will also work equally well. However, we found that there was a potential for significant confusion about how these three mechanisms interact with each other, so the next section will focus on that.

Details and Best Practices

One of the most frequently confused concepts we found when people were first given a chance to use BeforeTargets and AfterTargets was the issue of dependencies versus extensibility, and specifically how to use these mechanisms to enforce execution order.

When writing targets files, the author is primarily concerned with dependency ordering. That is, certain targets depend on certain other targets to run first. Importantly, the target which is being run must be able to specify that it requires those other dependency targets to be executed first, and that dependency will only be executed if the original target would execute. This strict dependency chain allows entire hierarchies of work to be described and controlled from a single target, and the entire hierarchy is known and understood when it is constructed. DependsOnTargets thus creates a static, design-time build hierarchy which MSBuild will turn into a run-time build order as the targets are executed.

Extensibility, on the other hand, is concerned with inserting new behaviors into an existing hierarchy. When BeforeTargets and AfterTargets are specified, they do not imply any dependency ordering. For instance, if 'GetSourceFiles' specifies a BeforeTargets of 'Build', this does not mean that 'GetSourceFiles' must run in order for 'Build' to run. Instead, it means that at the point where 'Build' would run, execute 'GetSourceFiles' first. The dependency hierarchy which already existed is unaffected, so targets specified by DependsOnTargets will still run if they would have before. Therefore, BeforeTargets and AfterTargetsalter run-time build ordering and do not themselves create or modify the build hierarchy.

Differences to DependsOnTargets

The biggest difference to DependsOnTargets in the behavior of BeforeTargets and AfterTargets is how the condition expression of the original target affects the running of the extension target. If target 'Build' specifies a DependsOnTargets of 'GetSourceFiles', the 'GetSourceFiles' target will not run if the condition of the 'Build' target evaluates to false. However, if you create a target 'GetSourceFiles' with BeforeTargets specifying 'Build', 'GetSourceFiles' will execute even if the condition on 'Build' evaluates as false. In this way, BeforeTargets and AfterTargets provide a way to unconditionally insert your extensions into an existing system. Of course you can still use conditions on the extension target itself to control its execution as necessary.

DependsOnTargets allows you to specify multiple targets, and those will be run in the order specified. So you can use that combined with BeforeTargets to ensure any combination of targets runs in the order specified before the target you are extending. Specifying multiple targets in the BeforeTargets attribute doesn't work the same way though – instead it just means that your target should run before any of the targets you have specified would execute. TheAfterTargets attribute has the same behavior except that your extension target will run after any of the specified targets would run.

BeforeTargets and AfterTargets allow you to specify targets which do not exist. This may be useful if you are creating a set of extension targets which are conveniently packaged in a single file but which may be applied to different kinds of projects with different targets. And, while you may use normal MSBuild expressions, such as property and item expressions to create your list of targets for BeforeTargets and AfterTargets, those expressions are evaluated only once when your project is loaded. This differs from DependsOnTargets which is evaluated at the time the target is run. Therefore you cannot use run-time mechanisms to generate values for these attributes.

Best Practices for Controlling Build Order

Understanding how ordering works between BeforeTargets, AfterTargets and DependsOnTargets is best illustrated by example. Consider the following:

     <Target Name="CleanSourceTree" BeforeTargets="Build">
        <Message Text="CleanSourceTree now executing" />
    </Target>
    <Target Name="GetSourceFiles" BeforeTargets="Build">
        <Message Text="GetSourceFiles now executing" />
    </Target>

The ordering of 'CleanSourceTree' and 'GetSourceFiles' relative to 'Build' is guaranteed (that is to say both 'CleanSourceTree' and 'GetSourceFiles' will in fact run before 'Build') but there is no guarantee of ordering between 'GetSourceFiles' and 'CleanSourceTree' (and wouldn't it be embarrassing to get your source files then immediately clear them before you build)! If you want to guarantee ordering, use a combination of DependsOnTargets with BeforeTargets. For instance, if you want 'CleanSourceTree' to run before 'GetSourceFiles', and both to run before 'Build', you would do the following:

     <Target Name="GetSourceFiles" BeforeTargets="Build" DependsOnTargets="CleanSourceTree">
        <Message Text="GetSourceFiles now executing" />
    </Target>
    <Target Name="CleanSourceTree">
        <Message Text="CleanSourceTree now executing" />
    </Target>

Looked at another way, 'GetSourceFiles' is your root extension target, and you use BeforeTargets to place it at a run-time location within the execution order of the project you are extending. You then build your own dependency hierarchy using DependsOnTargets.

Why not use BeforeTargets here instead of DependsOnTargets? The reason is that we are expressing a dependency here, not extending the build order. For example, if 'GetSourceFiles' is set up such that it will not execute if the source tree is up to date, then we certainly wouldn't want 'CleanSourceTree' to execute first, because it would delete our sources. With BeforeTargets, 'CleanSourceTree' would execute even if 'GetSourceFiles' skips. But with DependsOnTargets, 'CleanSourceTree' will never run if 'GetSourceFiles' is authored to skip when there is no work to do.

AfterTargets work the same way, so you must use DependsOnTargets to guarantee the run-order of your extension targets relative to each other. For instance, if you have two targets, 'CopyOutputsForPublishing' and 'PackageOutputFiles' which are supposed to run after 'Build', but you want 'CopyOutputsForPublishing' to run before 'PackageOutputFiles', you would use the same mechanism at the last example:

     <Target Name="PackageOutputFiles" AfterTargets="Build" DependsOnTargets="CopyOutputsForPublishing">
        <Message Text="PackageOutputFiles now executing" />
    </Target>
    <Target Name="CopyOutputsForPublishing">
        <Message Text="CopyOutputsForPublishing now executing" />
    </Target>

It is perfectly legitimate to specify both BeforeTargets and AfterTargets on the same target, though the effect will likely be the same as just specifyingBeforeTargets unless for some reason your target may skip the first time.

Conclusion

In summary, BeforeTargets and AfterTargets provide a new, powerful way for you to extend existing build systems without directly modifying them. They provide a mechanism independent of the structure of the original project for you to insert your behaviors while retaining the original mechanisms used to guarantee ordering between your extensions.

Cliff Hudson
Visual Studio Platform
MSBuild Developer