In case you’ve missed it, MSBuild is, essentially, the “new” version of that most ancient of build tools, MAKE.
Basically, it’s just a way to script out the steps required to build a project from end to end, possibly including
- Retrieving files from a Source Respository
- Altering them and checked the changes back in
- Compiling various application projects
- Copying files around
- Compiling an installation
- Sending email notifications of the build success or failure
and a host of other possibilities.
In the good ol’ days of MAKE and NMAKE, you had to use a particularly arcade file format to describe all this.
With MSBuild (oddly enough, it appears to come bundled with the .NET runtime, instead of one of the dev tools like Visual Studio, why is beyond me), you now configure everything through a relatively nice to look at XML file, usually named with a PROJ extension.
While MSBuild is fairly capable in it’s own right, there’s quite a number of things that typically need to happen during a build that it can’t handle directly, such as manipulating the contents of files, setting environment variables, etc.
That’s where the very thorough MSBuild Extensions pack comes in. Be sure to install it, then read the help file on how to hook up the XML Schema’s so that Visual Studio can show Intellisense for the extra functionality. It just requires copying a few XSD files around, so I won’t go into that here.
The Problem
One of the first things I had to do with the Extension Pack was alter the content of an RC (resource script) file that contains the version I need to use when stamping the files with a version number. I use a single, shared RC file so that it’s easy to update the version number for all files in the build.
My first shot was something like this:
<Target Name="UpdateBuildNumber"> <MSBuild.ExtensionPack.Framework.DateAndTime TaskAction="GetElapsed" Start="1 jan 2000" Format="Days"> <Output TaskParameter="Result" PropertyName="BuildNum"/> </MSBuild.ExtensionPack.Framework.DateAndTime> <Message Text="Build Number to use: $(BuildNum)"/> <ItemGroup> <CommonRCFile Include="Common.rc" /> </ItemGroup> <MSBuild.ExtensionPack.FileSystem.File TaskAction="Replace" RegexPattern="(?'Leader'\#define BUILD \s*).*" Replacement="${Leader}$(BuildNum)" Files="@(CommonRCFile)" /> </Target>
The Resource file itself looks like this (a snippet):
#define MAJOR 1 #define MINOR 5 #define REVISION 0 #define BUILD 100 #define COMPANYNAME "My Company" #define PRODUCTNAME "My Product"
What I was looking to do was replace that “100” for the build number with a number that corresponded to the number of days since Jan 1 2000, which would yield a nice number around 3470+, decent for a contantly incrementing build number and it fits within what a resource file can declare for the BUILD field.
I ran it can immediately ran into the first problem. The elapsed days was being returns in floating point, so I got a days value of something like 3745.5763527589. That would definitely NOT going to work as a build number.
After some tinkering, I ended up having to add a string SPLIT action to split off the integer days part, as in:
<MSBuild.ExtensionPack.Framework.TextString TaskAction="Split" String1="$(BuildNum)" String2="." StartIndex="0"> <Output PropertyName="BuildNum" TaskParameter="NewString"/> </MSBuild.ExtensionPack.Framework.TextString>
The trick here is that “StartIndex” property. That indicates which part of the split up string you want returned into the “Output PropertName” named property. A 0 means the first split element. Since I’m splitting on a “.” that will give me the number of days with no fractional portion.
I’m sure there’s a better way to do this, but I haven’t dug it up yet.
One problem down, so I ran the build and everything appeared fine till I compiled the RC file, at which point I got a very strange error about the file having unexpected values.
So I pulled up Araxis Merge to compare the old and new versions of the file, and something peculiar had happened. For the resource COPYRIGHT and TRADEMARK entries, I had used the copyright and registered ascii symbols (Ascii characters 169 and 174), but once the file went through that ExtensionPack FileSystem Replace action, those values had changes to something else entirely.
I suspected encoding was the culprit, and sure enough, there is a TextEncoding property for the Replace Action that apparently can control the encoding that is performed.
Ok, just find the right encoding…
- I tried UTF8…. Not recognized
- UTF-8…Recognized but same result
- ASCII…recognized but totally scrambled other parts of the file.
- ASCII-8… not recognized
- ASCII8… not recognized.
- US-ASCII…recognized, but just converted the suspect characters to ? chars.
At this point, I’m starting to get a little annoyed.
Eventually, I stumbled across a blog post that mentioned a “windows-1252” encoding and sure enough, Success at last! The build number is replaced, but the rest of the file is left as it was.
So the target entry I ended up with was:
<Target Name="UpdateBuildNumber"> <MSBuild.ExtensionPack.Framework.DateAndTime TaskAction="GetElapsed" Start="1 jan 2000" Format="Days"> <Output TaskParameter="Result" PropertyName="BuildNum"/> </MSBuild.ExtensionPack.Framework.DateAndTime>
<MSBuild.ExtensionPack.Framework.TextString TaskAction="Split" String1="$(BuildNum)" String2="." StartIndex="0"> <Output PropertyName="BuildNum" TaskParameter="NewString"/> </MSBuild.ExtensionPack.Framework.TextString> <Message Text="Build Number to use: $(BuildNum)"/> <ItemGroup> <CommonRCFile Include="Common.rc" /> </ItemGroup> <MSBuild.ExtensionPack.FileSystem.File TaskAction="Replace" RegexPattern="(?'Leader'\#define BUILD \s*).*" Replacement="${Leader}$(BuildNum)" Files="@(CommonRCFile)" TextEncoding="windows-1252"/> </Target>
One final comment. This required messing with .NET regular expressions, which I’m certainly no expert on.
I ended up coming across a very handy Regular Expression Tester by Francesco Balena (a favorite author of mine from way back), called YART. One .net EXE, just unzip and use. Supports find and replace, plus has context menus for all the various .NET Regex syntax bits and pieces. This definitely saved me a pile of time.
My opinion is still up in the air about MSBuild, but, with the Extension Pack, its capabilities are certainly quite formidable.
Give it a shot!