Posted by Will on Tuesday, May 27, 2008 at 10:13 PM

In the last episode I covered the installation of TeamCity and the associated MySQL database and said I would cover the configuration next.  I'm going to put that off for now and instead focus on MSBuild and the associated build script. 

Within TeamCity there are several available build runners.  From the TeamCity documentation a build runner is "a mechanism that executes a build of a certain type."  That's vague way of saying it will do the same thing that choosing "Build Solution" from the Build menu in Visual Studio will do - an oversimplification perhaps, but true.  So what's going on under the hood?  In this case, Visual Studio uses the settings in your solution and project files to compile your application or class library whether it be a debug or release build.  That is to say - the build script (a solution file) directs the actions of the build runner (Visual Studio).

TeamCity supports a dozen build runners, among which are sln2005, sln2008, and MSBuild.  The TeamCity documentation says to choose sln2005 or sln2008 if you want to use your .sln file from Visual Studio 2005 or 2008 respectively as the build script.  I started off down that track but quickly realized that I needed more control than I could get with a .sln file so I switched to MSBuild and used an MSBuild script.  In truth sln2005 uses MSBuild from .Net 2.0 while sln2008 uses the version of MSBuild from .Net 3.5.  The practical differences are in the configuration screens in the TeamCity setup.  For now, just be aware that MSBuild comes as part of the .Net framework installation.  You'll find it on your machine at <system drive>\Windows\Microsoft.NET\Framework\<version>\MSBuild.exe.  You can see it in action by opening a Visual Studio command prompt (Programs > Microsoft Visual Studio> Visual Studio Tools > Visual Studio 2008 Command Prompt) and entering the command msbuild followed by the full path to a .sln file.  Hit return and you'll see the build process.  It's the same thing that happens when you build from inside Visual Studio itself.   

As I mentioned earlier, MSBuild can use solution (.sln) files as build scripts although they do not have the same format as a true MSBuild project file.  Visual Studio project files do follow the format though. You can open one in NotePad to see the structure.  To get the whole thing started though you'll need a project file.  Start with something like this:

<Project DefaultTargets="" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>

I called mine BuildScipt.xml and used Visual Studio for editing.  You might prefer the XML Notepad tool from Microsoft.  As for the contents, MSDN has an overview of the file format for your reference.  In summary, there are several distinct sections:

PropertyGroup: Contains key/value pairs that can be referenced elsewhere in the script

<PropertyGroup>
    <TextResources>$(MSBuildProjectDirectory)\textfiles</TextResources>
    <SourceControlUserName>bobsuruncle</SourceControlUserName>
</PropertyGroup>

The $(MSBuildProjectDirectory) notation above uses a reserved word.  MSBuildProjectDirectory refers to the directory that contains the build script.  The TextResources key (one of my own making) above refers to a directory named "textfiles" that is within the directory containing the build script itself.  The $() notation is used to invoke a reserved word and is also used to reference the value of a PropertyGroup element via its key (see below).

ItemGroup: Contains references to inputs into the build

<ItemGroup>
    <FilesToInclude="$(TextResources)\readme.txt" />
    <FilesToInclude="$(TextResources)\license.doc" />
</ItemGroup>

Notice that in the "FilesToInclude" item group there are two files, readme.txt and license.doc.  They are found using the path described by the TextResources PropertyGroup element.  It is therefore equivalent to ".\textfiles\".  Later we can use @ character instead of $ (i.e., @(FilesToInclude)) to refer to this entire list of elements (see below). 

Target: Groups of tasks to be performed as a unit. 

<Target Name="CleanProject">
  <Message Text="******** Beginning Cleaning ********" />
  <!-- delete the InstallerComponentDirectory directory and its contents -->
  <RemoveDir Directories="$(InstallerComponentDirectory)" />
  <!-- delete the InstallerOutputDirectory directory and its contents -->
  <RemoveDir Directories="$(InstallerOutputDirectory)" />
  <!-- clean the build -->
  <MSBuild Projects=".\MySolution.sln" Targets="Clean" ContinueOnError="false" />
  <Message Text="******** Cleaning Complete ********" />
</Target>

This section introduces a few more concepts.  The name attribute is how you refer to the target elsewhere in the script.  Comments are designated with <!-- --> notation.  The Messages element is a preconfigured MSBuild task similar to Console.WriteLine() command for outputting information to the screen.  You will see this output while watching a build in TeamCity (as well as in the build log) so it makes for useful real-time feedback during the process.  The RemoveDir and MSBuild elements are also MSBuild tasks.  The MSBuild task is used to run the "Clean" command on the solution via the Targets attribute.  I could have just as easily put "release" or "debug" to run those builds.  In addition to the tasks that ship with MSBuild you can also make use of MSBuild Community Tasks which expands your options to include creating assembly files, FTP support, zip compression, registry manipulation, and more.  MSBuild Community Tasks is an open source project available as both an installer and source code.  Download the source code even if you don't need it because it includes the documentation as a help file.  If you get that annoying "Navigation to webpage was canceled" error when you open the help file remember to right-click on the file, choose properties from the context menu, and then click the Unblock button on the General tab to restore functionality.  To make use of the tasks run the MSBuild Community Tasks installer then add the following line just after the opening tag of your build script:

<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

For my HappyFish build script I wanted to be able to upload my changes to source control and with no other interaction end up with an installer on my Web site with the version number in the file name (i.e., HappyFish.2.0.63.117.msi).  The steps involved break down to:

  1. Create the assembly file(s) for the dll's and the exe to handle versioning
  2. Compile the solution
  3. Merge the assemblies
  4. Create the installer
  5. FTP the results

The hardest part was dealing with the build number.  I tried a lot of different techniques for tracking and managing build number incrementing but finally decided the least amount of friction was to let TeamCity handle it for me.  I'll have more on this in a later post, but TeamCity allows you to specify the build number format and seed for the build count value.  You can then refer to the build number using "$(build_number)" environment variable in your script wherever you need it. 

Returning to the list, step one consisted of making Assembly.cs files for the project components like so:

<Target Name="SomeComponentAssembly" >
  <Message Text="Writing SomeComponentAssembly file for $(build_number)"/>
  <!-- Create/update the assembly.cs file -->
  <AssemblyInfo CodeLanguage="CS"
   OutputFile="$(ProjectDirectory)\MyProject\Properties\AssemblyInfo.cs"
   AssemblyTitle="MyProject.MyAssembly"
   AssemblyDescription="My Project Library"
   AssemblyConfiguration=""
   AssemblyCompany="ThirstyCrow"
   AssemblyProduct="ThirstyCrow.MyProject.MyAssembly"
   AssemblyCopyright="Copyright © ThirstyCrow 2008"
   AssemblyTrademark="thirstycrow.net"
   ComVisible="false"
   CLSCompliant="true"
   Guid="9cd1823a-7c8f-4a0c-b740-0311ec695afe"
   AssemblyVersion="$(build_number)"
   AssemblyFileVersion="$(build_number)" />
</Target>

Complete documentation of the Assembly task can be found in the MSBuild help file by searching for assemblyinfo class members.  If want a quick way to create Guids check out this tip.

Second on the list is to compile the solution.  This is easily done with the MSBuild task show above.  By specifying that the StartMyBuild task below depends on another target, in this case SomeComponentAssembly then the tasks within that target will be run before the StartMyBuild tasks.  If you have more than one dependency you can enter them as a semicolon separated string (i.e., "SomeComponentAssembly;SomeOtherComponentAssembly;YetAnotherComponentAssembly"

<Target Name="StartMyBuild" DependsOnTarget="SomeComponentAssembly">
  <MSBuild Projects=".\MySolution.sln" Targets="Rebuild" ContinueOnError="false" />
  <Message Text="******** Build Complete ********" />
</Target>

You may or may not want to merge your assemblies, but if so you can use the ILMerge class of the MSBuild Community Tasks.  In the example below I've used the MakeDir MSBuild task to conditionally make a directory that will be used later to hold all of the components that will be combined into an installer project.  ILMerge is used to merge the list of components found in the "MergeFiles" ItemGroup.  NOTE: For some reason that I have yet to figure out if you use the $(propertygroup) notation in the OutputFile attribute of ILMerge it will fail so you'll have to put in the path explicitly.  However, it can be a relative path as shown.  Finally, as part of this target I've copied the supporting documents from the FilesToInclude (see above) - the readme and license documents - to the InstallerComponentDirectory that I'll be using later.

<Target Name="MergeAssemblies" >
  <Message Text="******** Beginning Merge ********" />
  <!-- create the InstallerComponentDirectory directory -->
  <MakeDir Directories="$(InstallerComponentDirectory)" Condition="!Exists('$(InstallerComponentDirectory)')"/>
  <!-- merge the assemblies -->
  <ILMerge TargetKind="WinExe" OutputFile=".Installer\ComponentDirectory\MyApplication.exe" InputAssemblies="@(MergeFiles)" DebugInfo="false"/>
  <!-- copy the files to the InstallerComponentDirectory directory -->
  <Copy SourceFiles="@(FilesToInclude)" DestinationFolder="$(InstallerComponentDirectory)" />
  <Message Text="******** Merge Complete ********" />
</Target>

For the installer I used Wix, which warrants a post or two of its own.  I'll skip over that for now and move on to FTP.  FTP is straight forward, just follow the documentation in the MSBuild Community Tasks found under FtpUpload Class.  My section looked something like this:

<Target Name="FtpProject">
  <Message Text="******** Uploading Files ********" />
  <FtpUpload
    Username="myusername"
    password="mypassword"
    RemoteUri="ftp://mydomain.com/builds/MyApplication.$(build_number).msi"
    LocalFile="$(InstallerOutputDirectory)\MyApplication.msi" />
  <Message Text="******** Uploading Complete ********" />
</Target>

You'll notice I made use of the build number environment variable provided by TeamCity to differentiate this build from others, thereby preventing naming collisions.

To test your build script you'll need a way to get the whole thing started.  Use the DefaultTargets attribute of the opening Project tag to tell MSBuild where to start.  Based on the example above I might use DefaultTargets="StartMyBuild".  This list can also be a semicolon separated list such as DefaultTargets="StartMyBuild;MergeAssemblies;FtpProject".  Save your changes and bring up the Visual Studio command prompt (see above) and run msbuild <path to your script>.  Be aware of the following caveats in troubleshooting:

  • References to the TeamCity specific $(build_number) environment variable will be empty strings, but it will not cause the script to fail.  The FtpProject output to the remote server would consequently be MyApplication..msi.  In production the space between the two dots in the filename would contain the build number.
  • If you use the copyright symbol © you'll need to make sure that the encoding of you build script is UTF-8.

At this point, if the script runs from the Visual Studio command prompt and you've used relative directory paths based on the MSBuildProjectDirectory reserved word you should be in good shape for using this script with TeamCity.  That will be the topic of the next thrilling episode of Adventures of a Lead Programmer in a One-man Shop.

Comments [0]     Categories: Continuous Integration | MSBuild