Code, photos, books, and anything else

Subscribe to my RSS feed

.NET Projects: Versioning Builds

Introduction

Continuing my .NET tooling series, and following on from starting with NAnt and revisiting NAnt to handle automated builds, I’ll now talk about assigning version numbers to your project builds.

Tagging your built product with a specific version number is an important process. It gives an indication of progress and evolution of the product. A full version number is formed of four segments, and resembles the following:

major.minor.build.revision

Each segment is briefly described below.

  • major – represents a significant version update
  • minor – indicates a less significant release, but still notable
  • build – an indication of how many times the project has been built
  • revision – represents minor updates

In .NET, each project in a solution has an AssemblyInfo file, which contains metadata about the assembly that the project compiles to. Visual Studio can automatically increment the build segment on each compile. This is rather basic, and requires manual editing of the other segments. Admittedly, adjusting the major and minor values should probably be a manual task anyway, but it would be nice to automate both the build and revision values. This is where NAnt comes in.

Versioning With NAnt

NAnt on its own does not offer any dedicated versioning tasks; NantContrib does offer a version task, which at first glance seems useful. However, on getting into the details of how it works, I didn’t like the options it offers for setting the build and revision numbers. There’s three options for the former, and two for the latter, none of which I really like.

So what do I do? As a coder, naturally, I devised a custom versioning technique.

Custom Versioning

As I said, I didn’t like the options provided by the NAntContrib version task, and thought it would be more useful to have a system in place to allow a custom versioning technique. This is done by taking advantage of the fact that NAnt can host and run .NET code – more on that later. The solution I devised involves keeping the version segments in memory as part of the NAnt “project” via custom properties. Those values can then be referenced anywhere within the context of the NAnt project, although they are only needed in a few places.

Tracking Versions

For this technique to work, I maintain a file containing the current version number. This file, shown below, is named version.txt and sits in the project’s root folder. There is nothing special about this file; it has a single line looking like:

0.1.0.0

As time goes on, and work is done on the project, the version is updated. The version file is kept in the project’s source control. Each time the version number is updated, so is the version file, so it must be committed with matching code revisions.

Following from the above, I needed a way to be able to reference the version number within the NAnt project, without having to load it from file each time. I set some properties near the top of my build file:

[code lang=”xml”] [/code]

There’s a property for each version number segment. They are provided initial values here, which get updated when the real current version number is loaded from the text file – that’s in the next section. Note also the fifth property, project.fullversion; the value is formed by joining the individual segments together. The dynamic attribute at the end is a useful one; when set to true, this property’s value is auto-updated when any of the individual segment values change. So at any time, to consume the full version number, only the project.fullversion property needs to be referenced.

Get Stored Version Number

I’ve set the stage, having indicated that the current version is persisted in a text file, and shown how the version, the segments and the full number, are stored in memory via the properties in the build file. It’s time to look at how to retrieve the current version from the text file and store it in memory via NAnt.

An interesting thing about NAnt is that while a build file is primarily made up of XML, .NET code can be embedded within a script element. Such code can be C# or VB; I’ve used C# here, but it can be converted without much trouble. A skeleton script element is shown below:

[code lang=”xml”][/code]

The script element has a language attribute which, clearly, tells NAnt which code language is being used. You can have multiple scripts, switching back and forth between C# and VB if you desire. It matters not to NAnt. In any case, you can do most anything you can in full .NET code; elements of the framework are available to you. This is a useful technique when you want to add custom code to a build process.

Within the script is a code element, which evidently indicates some code is about to happen. Seems a little redundant, but there it is. Further in, a CDATA element appears. NAnt needs to know that there is character data coming, and not XML. I believe this is required when using scripts in an XML file, NAnt or not. Now into the actual code.

The signature public static void ScriptMain(Project project) must be used to enclose custom code. It is a self-contained function, with the only tie to the NAnt project being the Project parameter. Through that, you gain access to the project’s properties from within the function.

Now, clearly the first step is to read the text file and get a copy of the contents. This is done as follows:

[code lang=”csharp”]string fileName = project.Properties[“file.versionNumber”];
StreamReader reader = new StreamReader(fileName);
string versionInfo = reader.ReadLine();
reader.Close();[/code]

The first line gets a local copy of the text file’s name, which is a property near the top of the proj.build file. Following that is a simple process of reading from the file, grabbing the contents, and closing the reader.

Next step is to parse the file contents into the segments we need:

[code lang=”csharp”]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));[/code]

The code uses a regular expression to separate the full version string into segments. There is some error checking to ensure that this results in four values.

The last thing to be done here is to store the individual project properties that were defined earlier in the build file.

[code lang=”csharp”]project.Properties[“version.major”] = matches[0].Value;
project.Properties[“version.minor”] = matches[1].Value;
project.Properties[“version.build”] = matches[2].Value;
project.Properties[“version.revision”] = matches[3].Value;[/code]

Just as you can read the project’s properties within a code block, you can also write to properties. That is done here; the property values can then be referenced via NAnt elsewhere in the build file, or by code in other code blocks. Remember what I said earlier about the full version number property with the dynamic attribute…By updating the segments here, the full number property is automatically updated.

Putting those lines of code together, the get-version task looks like this:

[code lang=”xml”]

[/code]

Note the echo element just before the task finishes. It makes sense to display the project’s current version number after getting it, so here it is.

Running the task via the command-line gives this output:

The get-version target being run by NAntThe get-version target being run by NAnt

This task is fairly simple; it reads the version.txt file, parses the contents into segments, and updates the in-memory project properties. There is also some error checking for the case that the format is not the way you expect it – too many sequences or too few. Finally, the composed version number is displayed on the command line as a reminder for the user.

Update the Version Number

At this point, the build process is able to retrieve the current version number and display it. What if the version number should be updated? I wrote a target to do just that. As with the get-version target, I’ll explain some related chunks of code, then show the code for the assembled target, along with a visual of the output.

First up is some preparation:

[code lang=”csharp”]string fileName = project.Properties[“file.versionNumber”];

DateTime now = DateTime.Now;
string nowString = now.ToString(“yy”) + now.DayOfYear.ToString();

int revision = int.Parse(project.Properties[“version.revision”]);
revision++;[/code]

The code above is setting things up to move forward with updating the build and revision values. The first line is getting the name of the file containing the version number, as shown for get-version. The following two lines get a DateTime object for “now” and create a string composed of some information from the DateTime. The intent is to have the build segment look like this:

09319

The first two digits are for the year, the later digits are for the day of year. This creates a reasonably precise mechanism to pinpoint when a particular build was done.

The last two lines get retrieve the current revision number from the project properties, convert to an integer (since properties are stored as strings), and increment the integer. A simple way to increase the build count.

The next step is to store the new and updated build and revision numbers, respectively.

[code lang=”csharp”]project.Properties[“version.build”] = nowString;
project.Properties[“version.revision”] = revision.ToString();[/code]

Simply a matter of storing the values back to the project properties, as shown before. Again, the property containing the full version number is automatically updated.

The new version number is in memory, but it needs to be sent to the version file:

[code lang=”csharp”]StreamWriter writer = new StreamWriter(fileName, false);
writer.WriteLine(project.Properties[“project.fullversion”]);
writer.Close();[/code]

Essentially the reverse of reading the number from the file in the previous target.

And here is the complete inc-version target.

[code lang=”xml”]

[/code]

Note the depends attribute: that ensures the current version is retrieved first. This ensures the correct current values are in memory before trying to manipulate them and send them back to file. And, again, the now-current version number is displayed for the user. Below is a screenshot of the command-line output when the task is run.

The inc-version target as run by NAntThe inc-version target as run by NAnt

Use Common AssemblyInfo

At this point, the build file has a mechanism to retrieve and display the current version number, and also a way to update the version and send it back to the file. What’s now needed is a way to apply the version number to a project build.

Each .NET project has an AssemblyInfo file containing metadata about the project, including the project’s version number. Typically, all projects within a Visual Studio solution have the same version number. It is possible to open each project file via NAnt and use some regular expression magic to update the version number in each, but there is a better way.

NAnt provides the asminfo task. It’s purpose: to generate an AssemblyInfo file with given parameters resulting in matching metadata values. This file can be shared via multiple projects, so for building purposes the version number only needs to be in the one common AssemblyInfo file. An extra benefit to this technique is that other common metadata can be extracted from the various AssemblyInfo files and placed in the common one. How that works will be explained shortly. But first, look at the following task:

[code lang=”xml”]

[/code]

This target starts with importing the NAntContrib tasks library. It then deletes any existing common AssemblyInfo file, since it is about to be regenerated. Following that, NAnt goes into the asminfo task. This task heavily uses properties I have defined in my build file – this is evident in the multiple instances of ${...} in the code. That’s is NAnt’s way of referencing properties defined elsewhere. In this case, most of the properties are in my standalone project properties file, which I showed in my previous post. If you’re familiar with .NET, you’ll recognize the metadata-equivalent property names within the task.

On the line starting the asminfo task, there are two attributes, output and language. The former references the same property as the delete line directly above, and indicates the location of the common AssemblyInfo file. The latter indicates the .NET language that the file should be generated in. The choices for which are VB and CSharp. Regardless of the language choice, the task functions in the same way. Both properties are set in the external properties file, as written about before.

The following attribute elements indicate which metadata values should be in the generated AssemblyInfo file. The values for the attributes are pulled from the external properties file.

My main build task depends on assemblyinfo, so the common file is always regenerated for each build. assemblyinfo depends on get-version, so that the current version number is always available before generating the common AssemblyInfo file. So there is a clear chain of targets to run when doing a build.

One more time, here is the output of this task when run on the command-line:

The assemblyinfo target being run via NAntThe assemblyinfo target being run via NAnt

So far as NAnt goes, that is all that needs to be done. There is one more step to have the common AssemblyInfo file picked up by a solution’s projects. Within Visual Studio, you can right-click a project, and choose Add->Existing Item. Navigate to the generated common AssemblyInfo file, select it, and move towards the OK button. But instead of just clicking the button, click the little arrow on the right side. That will show a small dropdown list, from which you want to choose Add as Link. The image below shows what I mean, in case it is not clear. This is important, because if you don’t add the file as a link, it will be copied into the project’s folder. Later updates to the common file would not affect the project’s local copy. Adding it as a link ensures the values in the common file are applied to the project when it is compiled. Repeat this process for each project within the solution.

Add the common AssemblyInfo file, as a link, to each projectAdd the common AssemblyInfo file, as a link, to each project

This will result in an extra item appearin in each project. It is essentially a shortcut to the common AssemblyInfo file. Simply having that shortcut within a project ensures that the common file’s contents are compiled into each project.

There is a final step. Using the Add as Link technique will result in a project having two AssemblyInfo files, with some overlap in the metadata they define. The overlapped metadata will cause errors at compile time. To fix the error, open each project’s own AssemblyInfo file – not the common one – and remove any metadata elements that are also present in the common AssemblyInfo file. I usually remove the following attributes:

  • Version
  • Company
  • Copyright
  • Configuration
  • Trademark
  • Product

The above attributes tend to be the same in each project and so can be externalized. Ensure that they are being added to the common AssemblyInfo file as shown earlier.

Also, since the common AssemblyInfo file will be regenerated on every build, there is no need to check it into your source control setup. You do want to check in the text file containing the version number, as that needs to be persistent.

Updating the Version, Building

With all of that done, it can be pretty simple to create a build with a new version number. The following command will do it:

build inc-version build

I have to explicitly call inc-version first, to update the version number, before I do a versioned build. That may also involve me manually adjusting the major and minor values within the version file, if needed.

The output will be similar to the following (click for bigger):

NAnt doing  a build with a new version numberNAnt doing a build with a new version number

For a regular build, the current version is retrieved, the common AssemblyInfo file is regenerated, and the projects get compiled. If I determine the time is right to do a versioned build, I can repeat the build process, but start it with the call to the inc-version target. Simple!

Conclusion

This article turned out a little longer than I had expected, but there was more information involved than I had anticipated. Particularly regarding the custom versioning tehnique, as well as showing how to centralize common assembly metadata. But at the end of it, the code given does cover the versioning concern, and suggests a way to reduce redundancy of repeated metadata. Both of which are worthy undertakings.

I reported the availability of the version task in NAntContrib, then bemoaned it’s lack of options. A good bit of time was then spent on how to retrieve the current version from a text file, update the build and revision values, and send the new version back to the file, ready to be retrieved the next time the version number was needed. Admittedly, it was some work, but there is now a sort of framework in place if ever a different versioning scheme is needed.

Following the versioning technique, a demonstration ensued on reducing redundancy in project metadata by introducing a common AssemblyInfo file, containing the metadata properties common to all projects in a solution, including the version number. Programmers don’t like redundancy in code, and in this case the metadata redundancy was also undesirable, since those properties are code as well!

That’s it for this installation on my .NET projects series. I have some options on what to write about next, so I need to decide that and get writing.

So, until the next article, happy versioning!

Author:

Format:

Category:
Programming

1 Comment
Comments closed

Comments

#1

John Goering

December 16, 2010 @ 6:36 am

Hey just wanted to say thanks for this article, it was very helpful and saved me a lot of time.

So…uh… “Thanks.” :)

Leave a Comment

Apologies, commenting is now disabled for this post.