Every .NET developer knows that we should version our assemblies. Not only does it keep you, the developer, sane, but it also helps installers work properly. File versioning is a fundamental task so it should be super easy, right? Let’s try it: create a new C# application and we’re greeted with the familiar AssemblyInfo.cs that contains:
GeSHi Error: GeSHi could not find the language csharp (using path D:\hosting\2839554\html\blog\wp-content\plugins\codecolorer\lib\geshi\) (code 2)
It’s a brand new project so we naturally update the version information to indicate that it’s still in development:
GeSHi Error: GeSHi could not find the language csharp (using path D:\hosting\2839554\html\blog\wp-content\plugins\codecolorer\lib\geshi\) (code 2)
If we build this new project the compiler cheerfully informs us:
warning CS1607: Assembly generation — The version ’0.1.*’ specified for the ‘file version’ is not in the normal ‘major.minor.build.revision’ format
Under the covers
What’s actually happening here is a good old fashioned overflow. By specifying “*” for the build number, the compiler uses the default format. That format is a date stamp of the form YYMMDD. The documentation for AssemblyFileVersion is not particularly helpful. But if we follow the breadcrumbs we’ll discover that every .NET AssemblyFileVersion gets translated to a VERSIONINFO resource in the underlying Windows OS. The MSDN documentation for the FILEVERSION portion of VERSIONINFO is more helpful:
Binary version number for the file. The version consists of two 32-bit integers, defined by four 16-bit integers. For example, “FILEVERSION 3,10,0,61″ is translated into two doublewords: 0x0003000a and 0x0000003d, in that order. Therefore, if version is defined by the DWORD values dw1 and dw2, they need to appear in the FILEVERSION statement as follows: HIWORD (dw1), LOWORD (dw1), HIWORD (dw2), LOWORD (dw2).
Or in other words, each component of a version must be less than 65,535. That means January 1, 2007 translates to 70,101 which overflows the 16 bits assigned to each portion of the version. The compiler is smart enough to detect the overflow, but the humans who wrote the compiler were not smart enough to give us a helpful error message. Bad programmers, no cookie!
If it were easy, everyone would do it
So how can we fix this issue? One solution is to remove the AssemblyFileVersion attribute and then specify the AssemblyVersion using wildcards. This certainly eliminates the warning: omitting the AssemblyFileVersion causes the AssemblyVersion to be used for the Win32 VERSIONINFO resource. The default format for the build portion of the AssemblyVersion is the number of days since January 1, 2000 and that will be less than 65,535 for quite a while. The default format for the revision portion is the number of seconds that have elapsed since midnight of the current day divided by 2 so that will always be less than 65,535. That sounds like a pretty easy solution….
Unfortunately, we’ve now introduced another problem: every time we rebuild this assembly its identity changes. A new version number goes into the assembly manifest with each compilation. For a stand alone application where everything is compiled in one solution using project references this may not be a problem. However, for software that is intended to be consumed by another solution via binary references, the consumers will quickly become unhappy with us. We’ve just forced them to pick one of the following terrible choices:
- Update all binary references in all project files with each new build. This is way too much work even if you automate it using NuGet.
- Set SpecificVersion to False for each binary reference. This is pretty easy, but then downstream consumers are not really sure what they’re getting at runtime. They’ll get some version of the file from somewhere on the system. There are rules for this sort of thing, but system administrators can override them with configuration files and who knows what an end user gets in a particular environment. (We’ll probably get a support call).
- Manually remove the version information from every binary reference in every project file. This option is not only time consuming, but it also results in partial assembly names. Microsoft recommends against it.
What is our happy place?
The ideal solution to this problem would have all of the following features:
- Specify AssemblyVersion independently of AssemblyFileVersion
- Allow the build and revision portions of AssemblyFileVersion to increment automatically with each build
- Build and revision portions of AssemblyFileVersion do not cause overflow warnings
Enter the MSBuild Community Tasks project. It’s a collection of MSBuild tasks that solves some common problems and short comings of .NET compilation. The Version task provides different options for setting the AssemblyVersion and AssemblyFileVersion attributes. We can follow the Version task with an AssemblyInfo task to generate a code file with assembly directives. The Version task is designed to be used in one of two ways:
- Pass version information directly to the task. Only the output .cs file is modified by this form. The build number is still computed as an epoch date, but the start of the epoch is specified by the StartDate attribute. The revision number is still the number of seconds roughly divided by 2.
GeSHi Error: GeSHi could not find the language xml (using path D:\hosting\2839554\html\blog\wp-content\plugins\codecolorer\lib\geshi\) (code 2) - Pass version information via a text file which gets updated by the task. The input text file and the output .cs file are both modified by this form.
GeSHi Error: GeSHi could not find the language xml (using path D:\hosting\2839554\html\blog\wp-content\plugins\codecolorer\lib\geshi\) (code 2)
We could add these tasks to every project. However, that could be quite a bit of work for an existing solution. It’s also a definite maintenance issue when new projects are created. (It sure would be nice if there was a GUI for editing a project’s MSBuild tasks, but apparently all we get is the XML editor.) Plus the revision portion will vary from one assembly to another since the clock continues to tick throughout our compilation cycle.
Pulling it all together
A better solution is to execute the tasks once, generate a code file with the version data, and then consume that code file in all other projects. This approach means we don’t need to modify every project file to include these custom build tasks. Plus we get the exact same version in all of our assemblies.
For indie devs who perform builds in the Visual Studio IDE, a reasonable solution is to create a project that contains these tasks as well as the output code file. Then add the output code file as a linked file in all other projects (Project -> Add Existing Item… -> Add As Link). Finally we need to manually adjust the project dependencies (via Project -> Project Dependencies…) so every project depends on our versioning project. This last step forces Visual Studio to build the versioning project before any others. This is an ongoing maintenance issue, but it’s a small one with which we can probably live.
For folks who are a little more sophisticated and invoke MSBuild directly, I’m sure there’s a way to dynamically generate an MSBuild project that drives the versioning and then kicks off the actual compile. Or something. This is left as An Exercise For The Reader™. (If you have a need to invoke MSBuild directly, you’re probably smart enough to figure out how to integrate this solution.) The same goes for people who use a build server: there’s some documentation somewhere that should tell you how to do it.
The MSBuild Community Tasks provide a great solution to the assembly and file versioning problem. It’s simple, light weight, and easy to consume. It just took quite a bit of digging to get there.








New game!
March 7th, 2012 byIts official, we have started a new game. Well, I suppose it would be more accurate to say that we have settled into working on our next game. Since this is all done in our very limited spare time we have to be pretty picky about what we work on next. The Bruce update is just about finished up and while that was going on we finally all congealed around an idea for our next title. I am really excited about our new project and can’t WAIT to share it with you. Ok, that’s not exactly true, I can wait…I would really just rather not. I still need to write that next Bruce post so be sure to keep watching for that as well.
There is something to be said about starting a new project like this. There is SOOOO much work ahead of us but it feels pretty good to be working on another project. Starting out, building the underpinnings (really isn’t my cup of tea, I’d rather be working on the pretty graphics), churning through design details and rules, getting concept art and prototype engine work done. It’s all very exciting and tempting to leap ahead to the fun stuff but you have to keep your focus and work smart. With a small team, there are three main things that you have to keep on your mind at all times, focus on the project, keep scope within the confines of reality, and stay motivated. If any of those things go off in a tail-spin, so do the others along with your progress and any hope of completing the game. (Queue dramatic music)
How do you stay motivated Justin? Well I’ll tell you! I’ve found that what works for one person is not necessarily what will work for the rest. I tend to stay motivated by fun tasks (graphics, game-play) and getting new art from the awesome art team (cheers for Beck!). In order to continue making progress I will spread out the things that I enjoy about making a game among the things that I don’t like, that way I always have something to look forward to. I am currently modeling all of our data into objects that will function as the basis for the entire game rule set. This data foundation has got to be done correctly or everything will be at risk…and I don’t believe I could care any less about it. It is a pain to work through this kind of thing for me but soon I’ll be able to get back to graphics and game-play. Any way, I just thought I’d give an update. If you have any questions about what we do just drop us a comment!
Thanks!
Posted in Commentary | No Comments »