V5: Fun with MSBuild

Published February 05, 2010
Advertisement
Now that Pulse is churning away at the codebase, I've spent time today doing further tidying of the build and deploy process. Once the site goes live I want to be able to get changes deployed quickly and safely, and I want to be able to start deploying builds for the Staff to look at within the next few days, so I'm doing what I can to get the pipeline right now. Fortunately, the tooling in this area is all really pretty good.

The first problem is versioning. As I mentioned in my last journal entry, I wanted to get MSBuild to stamp all my executables (I should have said 'assemblies' because a lot of these are libraries) with the build number. When dealing with rapidly-changing, opaque binaries spread across multiple computers, being able to ensure that your files are in sync is critical.

MSBuild, the standard build engine used for .NET projects, is highly flexible and extensible; it's very easy to just drop in new kinds of build task and add them to your project file, and best of all, Visual Studio is fine with it - it can't show them in the UI most of the time, but it can respect them, execute them, and generally not screw them up while working with the project file like normal. There's also a lot of drop-in build tasks freely available all over the net. For this, I'm using the AssemblyInfoTask (though I may upgrade to the MSBuild Extension Pack). The task takes the necessary version parts - major, minor, build, revision, plus any of the product/company name, copyright info etc that you usually find in a Win32 file version resource - and updates the project's AssemblyInfo.cs file with them prior to build. That's a little skeevy as it means the AssemblyInfo.cs file - which is under SVN - keeps getting local changes, but I can live with it. I've written a .targets file that incorporates the task just before the main compile phase, like so:

      1    0    $(PulseBuildNumber)    $(PulseSvnRevision)    NoIncrement    D    NoIncrement    D        "**\AssemblyInfo.*"
Exclude="**\.svn\**"/>


"bin/Release/AssemblyInfoTask.dll" TaskName="AssemblyInfo"/>



$(CoreCompileDependsOn);
UpdateAssemblyInfoFiles



"UpdateAssemblyInfoFiles"
Inputs="$(MSBuildAllProjects);
@(Compile);
@(ManifestResourceWithNoCulture);
$(ApplicationIcon);
$(AssemblyOriginatorKeyFile);
@(ManifestNonResxWithNoCultureOnDisk);
@(ReferencePath);
@(CompiledLicenseFile);
@(EmbeddedDocumentation);
@(CustomAdditionalCompileInputs);
@(AssemblyInfoFiles)"
Outputs="@(AssemblyInfoFiles);@(IntermediateAssembly)">
"@(AssemblyInfoFiles)"
AssemblyMajorVersion="$(AssemblyMajorVersion)"
AssemblyMinorVersion="$(AssemblyMinorVersion)"
AssemblyBuildNumber="$(AssemblyBuildNumber)"
AssemblyRevision="$(AssemblyRevision)"
AssemblyBuildNumberType="$(AssemblyBuildNumberType)"
AssemblyBuildNumberFormat="$(AssemblyBuildNumberFormat)"
AssemblyRevisionType="$(AssemblyRevisionType)"
AssemblyRevisionFormat="$(AssemblyRevisionFormat)">
"MaxAssemblyVersion" PropertyName="MaxAssemblyVersion"/>
"MaxAssemblyFileVersion" PropertyName="MaxAssemblyFileVersion"/>




This could be made somewhat more efficient - I don't strictly need to pull the version bits out into a separate PropertyGroup, for example, and could just write them directly into the attributes on the AssemblyInfo element. Still, it gets the job done. All I then need to do is add an statement into my .csproj file pointing at this .targets file, and the build step is magically included.

Note how the build and revision numbers are actually variables - PulseBuildNumber and PulseSvnRevision. I'm passing those in as arguments to MSBuild when I launch it. You can do this on the command-line using the /p switch, though because I'm using Pulse, it's actually got an XML config file that I use to feed inputs to MSBuild:
    

$(build.number) and $(build.revision) are, in turn, built-in variables defined by Pulse whenever it launches a build. See the data pipeline!

I had a good question from @naim_kingston on Twitter, asking why I use both the build number and the SVN revision number - aren't they redundant? In theory, yes; I should only need the SVN revision number, and then should be able to check out that revision of the code, build it, and always get the same result. In practice, though, I might not always get the same result because there are elements of the environment that may have changed. For example, maybe I'm using a different version of the compiler, or of the build tasks library. Storing the build number as well allows me to more quickly correlate a particular binary to its entry in Pulse's build log, so I can very quickly go to Pulse and download the right .pdb files, MSBuild output files, and so on, and always be confident that what I'm getting is from exactly the same build, rather than just one that used the same code.

So, that's got versioning sorted. I need to add the element to more of my project files, but I've got the main service projects covered for now. I'll add more as I go along.

Next, app.config files. It's common to want to change stuff in these files, such as the address at which a service can be found (e.g. from "db-server.gamedev.net" to "localhost"), but changing the app.config file directly means you have to remember not to check it into SVN, and it's kinda pesky to have it always showing up as 'modified' in the Pending Changes window. What would be better would be if I could have a second file of 'local overrides' that should be used in preference to the app.config file, falling back to app.config for stuff I don't care to change.

MSBuild to the rescue once more. This time I've used the MSBuild Community Tasks, which includes a task called "XmlMassUpdate" - given two XML files, it takes the nodes from one, and adds, inserts, or replaces them into the other. There's also some custom attributes for removing nodes from the target file. Another .targets file integrates the task into my build pipeline, and presto: I have an app.local.config file in each project, svn:ignored to stop it from pestering me, that MSBuild neatly integrates on every local build.

The next challenge I face is how to get each service from a ZIP file in Pulse to a correctly installed and registered presence on the relevant server. There's more to this than just XCOPY - most of the services need to be registered as Windows Services, have event log sources and WMI classes created, etc - and ideally it should happen without me logging into each machine by hand and copying files around + running InstallUtil. The answer is probably going to be to build MSI files. Anyway, that's for later. For now, I'll sleep.
0 likes 2 comments

Comments

NineYearCycle
I've just realised you use DevTrack, or at least your title image is from it :)

Sorry that is all I have to add.
February 06, 2010 01:10 PM
superpig
Hah! Yes, it's from DevTrack - had to use it a bit when doing some work on a project Activision were publishing, but never again. Never again. Ugh.
February 06, 2010 04:21 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement