Generated AssemblyVersion for NuGet package on TFS Build

5 minute read

Goal: SemVer & auto-incremented build number on package & project output

Imagine you want to auto-increment the build number in your AssemblyVersion during Continuous Integration, and meanwhile keep control over the first three version numbers (major.minor.patch). This would allow you to apply Semantic Versioning whilst generating new builds produce new assemblies/packages with a higher version number.

The NuGet command line allows you to fairly easy target a project file or a nuspec file (NuGet package manifest). If you are familiar with targeting a project file, you know you can use token placeholders for some parts of the package metadata, e.g. $id$, $version$ etc.

A rather annoying aspect of TFS Build lays in the fact that your compiled project output ends up redirected into a folder called Binaries (and there only!). If your build definition workspace is set to the solution directory (and you didn't modify this part of the Build Definition Template), this Binaries folder can be found here: $(SolutionDir)..\Binaries. Yes, one level up: it is a sibling of your checkout folder (called Sources) on the build agent.

There's no easy way out

There are some options at your disposal to get you going for a few hours trying to complete this puzzle.

  • The NuGet command line pack command has an extra option: BasePath
  • You'll have to play with relative paths to include only the stuff you want into your NuGet package
  • Which also means you'll need to create a NuGet package manifest (nuspec) upfront

I spent quite some time trying to figure out how to accomplish this with minimal changes. It actually turned out that there seems to be a bug in the NuGet command line, causing the BasePath to be ignored in certain cases (we are in case #3 if you follow that link).

This works for me

Prerequisites

  • Enable NuGet PackageRestore. This will give you the nuget.exe and some required MSBuild targets & settings in the $(SolutionDir).nuget folder.
  • Create a NuGet package manifest for your target project(s) (here's how) and use relative paths to include those files you need to be packaged from the Binaries folder:Â

    <files>
    <file src="....\Binaries\mylibrary.dll" target="lib\net40"/>
    </files>
    

  • Get the latest edition of the MSBuild Extension Pack from Codeplex (I used April 2012 edition)

  • Unzip that extension pack and find the following files in the 4.0.5.0 Binaries folder: Ionic.Zip.dll, MsBuildExtensionPack.dll *and *MSBuild.ExtensionPack.VersionNumber.targets. Add them to the $(SolutionDir).nuget\MsBuildExtensionPack folder and make sure you add them to source control.

Modifications

Modify the nuget.targets file you'll find in the $(SolutionDir).nuget folder to look like this:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="MsBuildExtensionPack\MSBuild.ExtensionPack.VersionNumber.targets" Condition="$(BuildPackage) == 'true'"/>
  <Import Project="NuGet.settings.targets"/>

  <Target Name="SetPackageVersion" Condition="$(BuildPackage) == 'true'">
    <PropertyGroup>
      <!-- Fetch the generated assembly version from the AssemblyInfo task (MSBuild Extension Pack, April 2012)-->
      <PackageVersion>$(MaxAssemblyVersion)</PackageVersion>

      <!-- If no NuSpec file defined in the project, fallback on the project itself-->
      <NuSpecFile Condition="$(NuSpecFile) == ''">$(ProjectPath)</NuSpecFile>

      <!-- Override BuildCommand with generated package version, if any -->
      <BuildCommand Condition="$(PackageVersion) != ''">"$(NuGetExePath)" pack "$(NuSpecFile)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols -version $(PackageVersion)</BuildCommand>
    </PropertyGroup>

    <!-- Log the generated package version if any -->
    <Message Text="Generated package version '$(PackageVersion)' from built assembly" Importance="high" />
  </Target>

  <Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
    <Exec Command="$(RestoreCommand)"
          LogStandardErrorAsError="true"
          Condition="Exists('$(PackagesConfig)')" />
  </Target>

  <Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites; SetPackageVersion">
    <Exec Command="$(BuildCommand)"
          LogStandardErrorAsError="true" />
  </Target>
</Project>

Edit the project file for which you want to create a NuGet package after compilation, and add the following MSBuild property in the Release configuration (or the configuration you use to build on TFS):

<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|Any CPU'">
   <!-- Shortened for brevity -->
   <NuSpecFile>relativePathFromProjectFileTo.nuspec</NuSpecFile>
</PropertyGroup>

Change the project's output location in the Release configuration (or the configuration you use to build on TFS) to *....\Binaries*

Controlling the Semantic Version

Note that you can't use any token placeholders with this solution. Actually, you might not need them anyway, since we set the (generated) version directly using the pack -version command.

To manage the semantic part of the version number (major.minor.patch), you can simply tweak some properties in the MsBuild.ExtensionPack.VersionNumbers.targets file. Yes, this is done manually, because there's nothing out there that can determine a semantic version for you.

Close to the top of that file, you'll find the properties that you need to tweak in order to change the first 3 version numbers.

<!-- Properties for controlling the Assembly Version -->
  <PropertyGroup>
    <AssemblyMajorVersion>1</AssemblyMajorVersion>
    <AssemblyMinorVersion>0</AssemblyMinorVersion>
    <AssemblyBuildNumber>0</AssemblyBuildNumber>
    <AssemblyRevision></AssemblyRevision>
    <AssemblyBuildNumberType></AssemblyBuildNumberType>
    <AssemblyBuildNumberFormat>00</AssemblyBuildNumberFormat>
    <AssemblyRevisionType>AutoIncrement</AssemblyRevisionType>
    <AssemblyRevisionFormat>00</AssemblyRevisionFormat>
  </PropertyGroup>

The build output looks like this:

Bonus: auto-push CI packages to MyGet

Or any other NuGet package repository for that matter. It is actually very easy to extend this setup in such way that the built package is automatically pushed to a CI feed for instance.

To minimize the modifications to the original nuget.targets and nuget.settings.targets files, I've put all my custom msbuild properties, overrides and targets into a separate nuget.Extensions.targets file, which I saved in the .nuget folder. As such, I only have to import the nuget.Extensions.targets file into the nuget.targets file, and make the BuildPackage target depend on the SetPackageVersion target. I've also added a conditional PushPackage call in the BuildPackage target.

Don't forget to enable auto-push by setting the new PushPackage MSBuild property to True in your project file, in the Release configuration (or the configuration you use in the build definition).

<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|Any CPU'">
    <OutputPath>..\..\Binaries\Release\</OutputPath>
    <BuildPackage>true</BuildPackage>
     <PushPackage>true</PushPackage>
    <NuSpecFile>relativePathFromProjectTo.nuspec</NuSpecFile>
</PropertyGroup>

Bonus 2: auto-push CI Symbols packages to SymbolSource

One of the interesting features MyGet offers for any private NuGet feed, is its integration with SymbolSource. Did you know that when you create a private feed on MyGet, you automatically get a private SymbolSource repository at your disposal, with the same shared API key? Maarten explains it here.

I simply added an additional PushSymbolsCommand and reproduced the same steps as for the PushPackageCommand. You can find my complete nuget.targets and nuget.Extensions.targets file below.

NuGet.targets

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 <Import Project="NuGet.settings.targets"/>
 <Import Project="NuGet.Extensions.targets" />

 <Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
  <Exec Command="$(RestoreCommand)" LogStandardErrorAsError="true" Condition="Exists('$(PackagesConfig)')" />
 </Target>

 <Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites; SetPackageVersion">
  <Exec Command="$(BuildCommand)" LogStandardErrorAsError="true" />

  <Exec Command="$(PushCommand)" LogStandardErrorAsError="true" Condition="Exists('$(NuPkgFile)') And $(PushPackage) == 'true'"/>

  <Exec Command="$(PushSymbolsCommand)" LogStandardErrorAsError="true" Condition="Exists('$(SymbolsPkgFile)') And $(PushPackage) == 'true'"/>
 </Target>
</Project>

NuGet.Extensions.targets

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 <Import Project="MsBuildExtensionPack\MSBuild.ExtensionPack.VersionNumber.targets" Condition="$(BuildPackage) == 'true'"/>
 <Target Name="SetPackageVersion" Condition="$(BuildPackage) == 'true'">
  <PropertyGroup>
   <!-- Fetch generated assembly version from AssemblyInfo task (MSBuild Extension Pack -->
   <PackageVersion>$(MaxAssemblyVersion)</PackageVersion>
   <PushPkgSource>http://www.myget.org/F/yourfeedname/</PushPkgSource>
   <SymbolsPkgSource>http://nuget.gw.symbolsource.org/MyGet/yourfeedname</SymbolsPkgSource>
   <PushApiKey>yourApiKey</PushApiKey>

   <!-- Property that enables pushing a package from a project -->
   <PushPackage Condition="$(PushPackage) == ''">false</PushPackage>

   <!-- Derive package file name in case the package will be pushed & nuspec is defined -->
   <NuPkgFile Condition="$(PushPackage) == 'true' And $(NuSpecFile) != ''">$(PackageOutputDir)\$(NuSpecFile.Trim('nuspec'))$(PackageVersion).nupkg</NuPkgFile>
   <SymbolsPkgFile Condition="$(PushPackage) == 'true' And $(NuPkgFile) != ''">$(NuPkgFile.Trim('nupkg')).Symbols.nupkg</SymbolsPkgFile>

   <!-- If no NuSpec file defined in the project, fallback on the project itself-->
   <NuSpecFile Condition="$(NuSpecFile) == ''">$(ProjectPath)</NuSpecFile>

   <!-- Override BuildCommand with generated package version, if any -->
   <BuildCommand Condition="$(PackageVersion) != ''">"$(NuGetExePath)" pack "$(NuSpecFile)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols -version $(PackageVersion)</BuildCommand>

   <!-- Added bonus: push command -->
   <PushCommand>"$(NuGetExePath)" push "$(NuPkgFile)" -source "$(PushPkgSource)" -apikey $(PushApiKey)</PushCommand>
   <PushSymbolsCommand>"$(NuGetExePath)" push "$(SymbolsPkgFile)" -source "$(SymbolsPkgSource)" -apikey $(PushApiKey)</PushSymbolsCommand>
  </PropertyGroup>

  <!-- Log the generated package version if any -->
  <Message Text="Generated version '$(PackageVersion)'" Importance="high" />
 </Target>
</Project>

I do hope TFS11 will bring a nice build template supporting this out-of-the-box, because this is exactly the kind of friction that I'd like to get rid of.

Leave a Comment