Increment Version for Changed Assemblies only – First part

This is something I did in the past (Build incrementale e numeri di versione [ITA]), and it came out recently on Stack Overflow, so I will take the time to describe how to do this in details.

The solution is design around MSBuild: leveraging some extension points, you trigger the custom code that increments some version number in AssemblyInfo.cs, subject to the same conditions that triggers code compiling. Being MSBuild based it will work both on a local (desktop) build, and a Team Foundation Build; you must be careful selecting the correct options for your Build Definition, otherwise it will not function. In this post we will look at the basics of inject code in the standard MSBuild process, leaving the Team Build customization for the next post. A good discussion about what version numbers are available and the best practices to manage them can be found in Team Foundation Build Customization Guide. This solution will not touch any Visual Studio / MSBuild project file (*.*proj) nor requires any change to a build server.

The basics

Let’s start analyzing how to intercept the standard compile process and inject a custom Task in it. The standard .Net compile process is designed with some extension points ready to use and since .Net 2; furthermore there are some properties to easily hook the extension points in a clean way (see How to: Extend the Visual Studio Build Process and Common MSBuild Project Properties for details). Strangely, the most powerful property is CustomAfterMicrosoftCommonTargets: supplying the path of an MSBuild file, all targets defined in your file redefine the default ones and add more. Let’s see an example. Suppose you created a new Console Application project in the C:\Sample folder; running from the command line

  MSBuild /property:CustomAfterMicrosoftCommonTargets=c:\Sample\custom.targets ConsoleApplication1.csproj

and assuming custom.targets contains

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="AfterCompile">
    <Message Text="*TRACE* AfterCompile intercepted!" Importance="high" />
  </Target>
</Project>

the output for the first run of the command is

image

you see that AfterCompile is redefined, and is executed after compiling the project. Rerunning the command without touching any file gives a different output, as MSBuild is smart to not recompile when source is unchanged.

image

Note that our custom message appears in both scenarios.

MSBuild side

Now that we have seen the basics of intercepting the standard compile process, let’s see how to leverage this technique to control the versioning. In order to see an updated version number on the executable, we have to select a target running before the target which compiles the project; also it must be run only if the project will be compiled. For this to happen, use the BeforeCompile target and specify the same Inputs and outputs that the CoreCompile target uses, as in the following MSBuild snippet.

<Target
    Name="BeforeCompile"
    Inputs="$(MSBuildAllProjects);
        @(Compile);                               
        @(_CoreCompileResourceInputs);
        $(ApplicationIcon);
        $(AssemblyOriginatorKeyFile);
        @(ReferencePath);
        @(CompiledLicenseFile);
        @(LinkResource);
        @(EmbeddedDocumentation); 
        $(Win32Resource);
        $(Win32Manifest);
        @(CustomAdditionalCompileInputs)"
    Outputs="@(DocFileItem);
         @(IntermediateAssembly);
         @(_DebugSymbolsIntermediatePath);                 
         $(NonExistentFile);
         @(CustomAdditionalCompileOutputs)">
    <!-- increment task here -->
</Target>

The standard process computes the project inputs — source files, resources, referenced projects and DLLs — the project outputs — executables, PDBs, XML documentation files, etc — and compare the timestamps to see if outputs are out-of-date with respect to inputs. It is important to list the same set of files as CoreCompile, so MSBuild will trigger our target on the same condition used to compile the source. This code can be found in the %Program Files (x86)%\MSBuild\12.0\Bin\Microsoft.CSharp.CurrentVersion.targets file for MSBuild 12 (Visual Studio 2013 or later) or %SystemRoot%\Microsoft.NET\Framework\v4.0.30319\Microsoft.CSharp.targets (up to Visual Studio 2012 included).

Updating the Assembly version is easy thanks to the AssemblyInfo task of MSBuild Extension Pack.

<ItemGroup>
    <AssemblyInfoFilesToUpdate Include=”$(MSBuildProjectDirectory)\Properties\AssemblyInfo.cs”/>
</ItemGroup>

<MSBuild.ExtensionPack.Framework.AssemblyInfo
    AssemblyInfoFiles=”@(AssemblyInfoFilesToUpdate)”
    AssemblyFileBuildNumberType=”YearWeekDay”
    AssemblyFileRevisionType=”AutoIncrement”
    FirstDayOfWeek=”Sunday”
/>

In this snippet we collect the AssemblyInfo file contained in the Project’s Properties folder and feed it to the Task asking to set a build number using current date and to automatically increment the revision number; major and minor are left untouched. See the Task documentation for many more options. Only two DLLs are required: MSBuild.ExtensionPack.dll and Ionic.Zip.dll.

required DLLs

The code does not change the AssemblyVersion attribute: this is the most common option and in the Team Foundation Build Customization Guide you will find in depth information on the reasons.

In the next post we will see how to integrate these techniques in the Team Foundation Build process.