Managaging assembly versioning in NAnt

Recently I ran a lab for the Denver Visual Studio User Group on open source tools NAnt, NUnit, Subversion, and Cruise Control.Net, tools that significantly improve the development cycle in .Net. I briefly touched on the NAnt feature allowing C#, VB, or J# code to be included in your NAnt file, and dynamically compiled and run by NAnt during a build. I use this feature to handle versioning of assemblies the way I want the versioning to work. One of the lab participants requested a copy of this code. As always, feel free to provide feedback!

I realize that today's version of NAnt allows an assemblyinfo.cs file to be built dynamically in the script using the <asminfo> task, providing some flexibility in how versioning and signing is handled during the build. When I put this together, either that feature didn't exist or I wasn't aware of it. Thus, my approach involves parsing assemblyinfo.cs files and substituting version numbers at build time - you may be able to improve on this. As I look at it, there are other things I could do to improve and simply it, but I'll just include the currently working copy for now, and let you adapt it to your needs.

My versioning model is based on a control file containing the last-used version number. I increment the third portion of the number during a build, and commit this change to a Subversion repository if the build was successful. This way version numbers are only assigned to successful builds, and I don't waste version numbers on builds that failed. Of course, failed builds never leave the build server.

The first target is getversion, which pulls the portions of the version number out of the version.txt file into properties:

<target name="getversion" description="Populate the version properties based on the version.txt file">
  <echo message="${nant.project.basedir}"/>
  <script language="C#">
   <code><![CDATA[
    public static void ScriptMain(Project project)
    {
     // parse version document to get version information
     string fileName = Path.Combine(project.BaseDirectory, project.Properties["build.version.filename"]);
     StreamReader reader = new StreamReader(fileName);
     string versionInfo = reader.ReadLine();
     reader.Close();
     Regex pattern = new Regex("[0-9]+");
     MatchCollection matches = pattern.Matches(versionInfo);
     if (matches.Count != 4)
      throw new Exception(string.Format("Version number {0} in {1} has incorrect format.", versionInfo, fileName));
     int major = int.Parse(matches[0].Value);
     int minor = int.Parse(matches[1].Value);
     int build = int.Parse(matches[2].Value);
     int revision = int.Parse(matches[3].Value);
     project.Properties.Add("build.version.major", major.ToString());
     project.Properties.Add("build.version.minor", minor.ToString());
     project.Properties.Add("build.version.build", build.ToString());
     project.Properties.Add("build.version.revision", revision.ToString());
    }
   ]]></code>
  </script>
  <call target="setversionstring" />
 </target>
 
 
This calls setversionstring, which simply combines the seperate properties into a single versionstring property:
 
 <target name="setversionstring" description="Set the build.version.versionstring property">
  <script language="C#">
   <code><![CDATA[
    public static void ScriptMain(Project project)
    {
     string versionString = string.Format("{0}.{1}.{2}.{3}",
      project.Properties["build.version.major"],
      project.Properties["build.version.minor"],
      project.Properties["build.version.build"],
      project.Properties["build.version.revision"]
     );
     project.Properties["build.version.versionstring"] = versionString;
     project.Log(Level.Info, versionString);
    }
   ]]></code>
  </script>
 </target>

 
The incrementbuildnumber target is called from server builds (not developer-machine builds) to increment the third section of the number:
 
 <target name="incrementbuildnumber" description="Increment the build number and write to version.txt file">
  <script language="C#">
   <code><![CDATA[
    public static void ScriptMain(Project project)
    {
     string fileName = Path.Combine(project.BaseDirectory, project.Properties["build.version.filename"]);
     int major = int.Parse(project.Properties["build.version.major"]);
     int minor = int.Parse(project.Properties["build.version.minor"]);
     int build = int.Parse(project.Properties["build.version.build"]);
     int revision = int.Parse(project.Properties["build.version.revision"]);
     build++;
     string versionString = string.Format("{0}.{1}.{2}.{3}", major, minor, build, revision);
     project.Properties["build.version.build"] = build.ToString();
     
     StreamWriter writer = new StreamWriter(fileName, false);
     writer.WriteLine(versionString);
     writer.Close();
    }
   ]]></code>
  </script>
  <call target="setversionstring" />
 </target>

 
Finally, setversion is used to update the assemblyinfo.cs files with the updated version number:
 
 <target name="setversion" description="Stamp the version info onto assemblyinfo.cs files">
  <foreach item="File" property="filename">
   <in>
    <items basedir="application">
     <include name="**\AssemblyInfo.cs"></include>
    </items>
   </in>
   <do>
    <script language="C#">
     <code><![CDATA[
     public static void ScriptMain(Project project)
     {
      //FileStream file = File.Open(project.Properties["filename"], FileMode.Open, FileAccess.ReadWrite);
      StreamReader reader = new StreamReader(project.Properties["filename"]);
      string contents = reader.ReadToEnd();
      reader.Close();
      string replacement = string.Format(
       "[assembly: AssemblyVersion(\"{0}.{1}.{2}.{3}\")]",
       project.Properties["build.version.major"],
       project.Properties["build.version.minor"],
       project.Properties["build.version.build"],
       project.Properties["build.version.revision"]
      );
      string newText = Regex.Replace(contents, @"\[assembly: AssemblyVersion\("".*""\)\]", replacement);
      StreamWriter writer = new StreamWriter(project.Properties["filename"], false);
      writer.Write(newText);
      writer.Close();
     }
     ]]>
     </code>
    </script>
   </do>
  </foreach>
 </target>
 
This way, when the project compiles, our version number will be used on each assembly. For developer builds, I hard-code the "build.version.build" to 0, so that I can identify the build as a developer build. This is accomplished with the following task, called after getversion:
 
  <property name="build.version.build" value="0" />
 
I trust that this will be helpful!
 
- Josh
Published Tuesday, July 26, 2005 9:56 PM by Joshua Langemann
Filed under:

Comments

Friday, July 29, 2005 7:11 AM by Joshua Langemann

# Link to lab materials

The materials from the lab can be found at this URL: ftp://66.180.109.178/pub/VBSig/Labs/2005/07_22_2005
Wednesday, October 18, 2006 2:52 PM by Bite my bytes

# Setting version number in AssemblyInfo files with NAnt

Sunday, November 05, 2006 7:34 PM by C# .Net Tales

# Release Management

I&#39;ve been looking into Release management and versioning assemblies in .Net 2.0. Joshua shows a good

Wednesday, September 05, 2007 10:38 PM by Mirrored Blogs

# Release Management

Body: I&#39;ve been looking into Release management and versioning assemblies in .Net 2.0. Joshua shows

# Migrating from CruiseControl.NET to TeamCity 3.1 | voodude.ch/blog

Pingback from  Migrating from CruiseControl.NET to TeamCity 3.1 | voodude.ch/blog

Leave a Comment

(required) 
(required) 
(optional)
(required)