Austin Agile DevOps

DevOps in the Cloud
posts - 70 , comments - 7 , trackbacks - 0

Team Build 2010 Custom Activity for Assembly Version Stamping

This approach is for those who have the following requirements:

  1. Use a Primary.Secondary.Maintenance.Build (P.S.M.B) version number format
  2. Want the build name to include the version number
  3. Want the assemblies "stamped" with version number
  4. Don't want to checkin the increments to AssemblyInfo.cs files for every build
  5. Don't want to modify the .csproj file for their projects

To illustrate:

  • Our build definition name format is: [product abbreviation]-[p].[s]{.[m]}-[type]. The type indicates a Dev, Test, Rel, etc... build.
  • An example build definition name is: KApp-3.2-Dev.
  • Our build number format is:$(BuildDefinitionName)$(Rev:.rrr)
  • From this we get build names like: Kapp-3.2-Test.012

The approach is as follows:

  1. Parse the P.S, and if used, .M from the build definition name. Actually, I parse it from the build folder name because it seemed easier than getting the build definition name.
  2. Get the build number from TFS
  3. Assemble the version number
  4. Run over all the AssemblyInfo files and use RegX to replace the file and version number

The details are shown in the code below:

NOTE: for more information on how to implement custom activities in Team Build 2010, see http://www.ewaldhofman.nl/?tag=/build+2010+customization

<!-- code formatted by http://manoli.net/csharpformat/ -->

using System;
using System.Activities;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.TeamFoundation.Build.Client;

namespace BuildTasks.Activities
{
  [BuildActivity(HostEnvironmentOption.Agent)]
  public sealed class UpdateAssemblyInfoFiles : CodeActivity
  {
    // The file mask of all files for which the build number of the
    // assembly version must be increased
    [RequiredArgument]
    public InArgument<string> AssemblyInfoFileMask { get; set; }

    // The SourcesDirectory as initialized in the build process template
    [RequiredArgument]
    public InArgument<string> SourcesDirectory { get; set; }
    public InArgument<string> BuildNumber { get; set; }
    public InArgument<string> BuildDirectory { get; set; }

    public OutArgument<string> TextOut { get; set; } // debug code

    protected override void Execute(CodeActivityContext context)
    {
      // Obtain the runtime value of the input arguments
      string sourcesDirectory = context.GetValue(this.SourcesDirectory);
      string assemblyInfoFileMask = context.GetValue(this.AssemblyInfoFileMask);
      string buildNumber = context.GetValue(this.BuildNumber);
      string buildDirectory = context.GetValue(this.BuildDirectory);
      
      // Parse build definition name from build directory
      int nameStart = (buildDirectory.LastIndexOf(@"\") + 1);
      string buildDefinitionName = buildDirectory.Substring(nameStart);

      // Parse the primary.secondary.maintenance version numbers from the build definition name
      // The format of the build definition name is [product abbreviation].P.S{.M}-[Build Type]

      // Find the start and end of the build definition version number
      int verStart = (buildDefinitionName.IndexOf("-") + 1);
      int verEnd = buildDefinitionName.IndexOf("-", verStart);

      // Parse out the version number
      string version = buildDefinitionName.Substring(verStart, (verEnd - verStart));

      // Count the number of decimal points in the version number
      MatchCollection charColl = Regex.Matches(version, @"[\.]+");
      int numD = Convert.ToInt16(charColl.Count);

      // Get the version numbers
      string p = string.Empty;
      string s = string.Empty;
      string m = string.Empty;
      int primaryVer = 0;
      int secondaryVer = 0;
      int maintenanceVer = 0;

      int d1 = version.IndexOf(".");
      p = version.Substring(0, d1);

      if (numD == 1)
      {
        s = version.Substring((d1 + 1), (version.Length - (d1 + 1)));
      }
      else
        if (numD == 2)
        {
          int d2 = version.IndexOf(".", (d1 + 1));
          s = version.Substring((d1 + 1), (d2 - (d1 + 1)));
          m = version.Substring((d2 + 1), (version.Length - (d2 + 1)));
        }
        else
        {
          throw new ArgumentException("ERROR - the number of decimal points should be only 1 or 2, not: " + numD);
        }

      // debug code: use build definition verbosity of diagnostic to view in build report
      context.SetValue<string>(this.TextOut, "(debug) - build Number = " 
                              + buildNumber
                              + ", buildDirectory = "
                              + buildDirectory
                              + ", buildDefinitionName = "
                              + buildDefinitionName
                              + ", version = "
                              + version
                              + ", p = "
                              + p
                              + ", s = "
                              + s
                              + ", m = "
                              + m);

      // Set values of the build definition version number
      primaryVer = Convert.ToInt16(p);
      secondaryVer = Convert.ToInt16(s);
      
      // leave maintenanceVer == 0 if not used in buildDefinitionName
      if (m != string.Empty)
      {
        maintenanceVer = Convert.ToInt16(m);
      }

      // Parse the build increment number from the actual build number
      int buildIncrementNumberStart = (buildDefinitionName.Length + 1);
      string buildIncrementValue = buildNumber.Substring(buildIncrementNumberStart, (buildNumber.Length - buildIncrementNumberStart));

      //remove any leading zeros
      while (buildIncrementValue.StartsWith("0"))
      {
        buildIncrementValue = buildIncrementValue.Remove(0, 1);
      }

      int buildIncrementNumber = Convert.ToInt16(buildIncrementValue);

      // Enumerate over all version attributes
      foreach (string attribute in new string[] { "AssemblyVersion", "AssemblyFileVersion" })
      {
        // Define the regular expression to find (which is for example 'AssemblyVersion("1.0.0.0")' )
        Regex regex = new Regex(attribute + @"\(""\d+\.\d+\.\d+\.\d+""\)");

        // Get all AssemblyInfo files
        foreach (string file in Directory.EnumerateFiles(sourcesDirectory, assemblyInfoFileMask, SearchOption.AllDirectories))
        {
          string text = File.ReadAllText(file);  // Read the text from the AssemblyInfo file
          Match match = regex.Match(text);    // Search for the first occurance of the version attribute

          if (match.Success)           // When found
          {
            // Update build increment number in the version and write out to AssemblyInfo files
            Version newVersion = new Version(primaryVer, secondaryVer, maintenanceVer, buildIncrementNumber);

            // Get file attributes
            FileAttributes fileAttributes = File.GetAttributes(file);

            // Set file to read only
            File.SetAttributes(file, fileAttributes & ~FileAttributes.ReadOnly);

            // Replace the version number 
            string newText = regex.Replace(text, attribute + "(\"" + newVersion.ToString() + "\")");

            // Write the new text in the AssemblyInfo file
            File.WriteAllText(file, newText);

            // restore the file's original attributes
            File.SetAttributes(file, fileAttributes);
          }
        }
      }
    }
  }
}

Print | posted on Tuesday, August 3, 2010 2:12 PM | Filed Under [ Agile SCM Talk Blog ]

Feedback

No comments posted yet.
Post A Comment
Title:
Name:
Email:
Comment:
Verification:
 

Powered by: