Way back in the deep and distant past I started discussing building systems using MSBuild. And promptly started ignoring the topic. So now I’m back with the build file I’m currently using in one of my personal projects.
This build file is designed to version, build, test and package the system when invoked by a build server (in this case TeamCity). To do so it relies on the Gallio Automation Platform and the MSBuild Community Tasks Project. In the build process this file replaces the solution (.sln) file but uses the existing project files (.csproj) which are in MSBuild format. As with the previous version the build file relies on grouping related categories of projects into directories in order to allow different operations to be performed that are relevant to each category. This file uses two categories; Main which uses the Src directory and contains projects that build the actual deliverables of the system and Test which uses the Tests directory and contains the unit test projects. Other categories may also be supported. I would recommend keeping the number contained to prevent the build process becoming brittle and difficult to maintain.
In order to support packaging of the build output the project categories will also build into common directories. The Main category builds into the directory BuildOutput and the Test category into TestOutput, Technically it is only necessary to have the Main category build into a common directory as the test assemblies are never packaged. I find it more convenient to have consistency in this case (this is subjective and your experience may vary). I do not bother with separate directories for build configurations (Debug or Release) as is the standard for Visual Studio as I find this to be an additional complexity without actual benefit in this scenario. As I will only take release builds from the build server which is configured only to produce a consistent output using a single configuration I do not have the need to separate files compiled using different configurations.
The build file looks like this:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<PropertyGroup>
<MSBuildCommunityTasksPath>.</MSBuildCommunityTasksPath>
<AbstractCodeFrameworkMajorVersion>1</AbstractCodeFrameworkMajorVersion>
<AbstractCodeFrameworkMinorVersion>3</AbstractCodeFrameworkMinorVersion>
<AbstractCodeFrameworkBuildVersion>2</AbstractCodeFrameworkBuildVersion>
<AbstractCodeFrameworkRevision>0</AbstractCodeFrameworkRevision>
<AbstractCodeFrameworkVersion>$(AbstractCodeFrameworkMajorVersion).$(AbstractCodeFrameworkMinorVersion).$(AbstractCodeFrameworkBuildVersion).$(AbstractCodeFrameworkRevision)</AbstractCodeFrameworkVersion>
<FrameworkBuildNumber>$(BUILD_NUMBER)</FrameworkBuildNumber>
</PropertyGroup>
<Import Project="..\Tools\MSBuild.Community.Tasks\MSBuild.Community.Tasks.Targets"/>
<UsingTask AssemblyFile="..\Tools\Gallio\Gallio.MsBuildTasks.dll" TaskName="Gallio"/>
<ItemGroup>
<MainProjects Include="Src\**\*.csproj"/>
<TestProjects Include="Tests\**\*.csproj"/>
<AllProjects Include="@(MainProjects);@(TestProjects)"/>
</ItemGroup>
<Target Name="CleanPackage">
<RemoveDir Directories="Package" Condition="Exists('Package')"/>
<RemoveDir Directories="BuildOutput" Condition="Exists('BuildOutput')"/>
<RemoveDir Directories="TestOutput" Condition="Exists('TestOutput')"/>
</Target>
<Target Name="Clean" DependsOnTargets="CleanPackage">
<MSBuild Projects="@(AllProjects)" Targets="Clean"/>
</Target>
<Target Name="AssemblyInfo" DependsOnTargets="CleanPackage">
<Version BuildType="Automatic"
RevisionType="Automatic"
Major="$(AbstractCodeFrameworkMajorVersion)"
Minor="$(AbstractCodeFrameworkMinorVersion)"
Build="$(AbstractCodeFrameworkBuildVersion)"
Revision="$(AbstractCodeFrameworkRevision)">
<Output TaskParameter="Major" PropertyName="FileVersionMajor" />
<Output TaskParameter="Minor" PropertyName="FileVersionMinor" />
<Output TaskParameter="Build" PropertyName="FileVersionBuild" />
<Output TaskParameter="Revision" PropertyName="FileVersionRevision" />
</Version>
<PropertyGroup>
<AbstractCodeFrameworkFileVersion>$(FileVersionMajor).$(FileVersionMinor).$(FileVersionBuild).$(FileVersionRevision)</AbstractCodeFrameworkFileVersion>
</PropertyGroup>
<AssemblyInfo CodeLanguage="CS"
OutputFile="GlobalAssemblyInfo.cs"
AssemblyConfiguration=""
AssemblyCompany=""
AssemblyProduct="AbstractCode Framework Build $(FrameworkBuildNumber)"
AssemblyCopyright="Copyright © Colin David Scott 2007 - 2009"
AssemblyVersion="$(AbstractCodeFrameworkVersion)"
AssemblyFileVersion="$(AbstractCodeFrameworkFileVersion)" />
</Target>
<Target Name="Build" DependsOnTargets="AssemblyInfo">
<MSBuild Projects="@(MainProjects)" Targets="Rebuild">
<Output TaskParameter="TargetOutputs" ItemName="BuildOutput"/>
</MSBuild>
</Target>
<Target Name="Test" DependsOnTargets="Build">
<RemoveDir Directories="TestOutput\Report" Condition="Exists('TestOutput\Report')"/>
<MakeDir Directories="TestOutput\Report"/>
<MSBuild Projects="@(TestProjects)" Targets="Rebuild">
<Output TaskParameter="TargetOutputs" ItemName="TestOutput"/>
</MSBuild>
<Gallio IgnoreFailures="false"
Assemblies="@(TestOutput)"
ReportDirectory="TestOutput\Report"
ReportTypes="html;xml"
RunnerExtensions="TeamCityExtension,Gallio.TeamCityIntegration" >
<Output TaskParameter="ExitCode" PropertyName="ExitCode"/>
</Gallio>
</Target>
<Target Name="Package">
<MakeDir Directories="Package"/>
<ItemGroup>
<FrameworkFiles Include="BuildOutput\*.*" Exclude="**\*.pdb"/>
</ItemGroup>
<Zip Files="@(FrameworkFiles)" ZipFileName="Package\AbstractCode Framework $(AbstractCodeFrameworkVersion) %28$(FrameworkBuildNumber)%29.zip" WorkingDirectory="BuildOutput"/>
</Target>
</Project>
At the start of the file we define a number of common data items that are applicable to the build process. The includes the version number for the result as well as the build number (populated from an enviornment variable set by TeamCity) and some configuration used by the MSBuild Community Tasks. We also import the Gallio testrunner and MSBuild Community Tasks for later usage.
Next we specify variables that locate the project files we are building. This is done using wildcard searches for .csproj files and relies on the solution having the correct directory structure. This is generally more accurate than relying on naming conventions for the projects, but will likely require the reorganisation of the structure of an existing solution. In addition to defining a variable for each group we define an all group that contains the projects in all the other groups. This is helpful in performing actions globally.
We then define a series of targets:
- CleanPackage is used to remove the directory into which the packaged output is located. It also removes the directories I use as the common build locations for deliverable and test projects. If you are using the standard build directories then this target need only delete the Package directory.
- Clean invokes the Clean target on all the project files. It has a dependency on CleanPackage to remove other build artefacts.
- AssmblyInfo is used to generate a common file that contains the version information and related assembly metadata to be assigned to all build outputs. It generates the file as GlobalAssemblyInfo.cs. Each project includes this as a linked file in addition to its own AssemblyInfo.cs file (which stores metadata specific to that assembly). This target used the Version and AssemblyInfo tasks from the MSBuild Community Tasks. Version generation is discussed below. This target depends on CleanPackage to ensure that the build server will not pick up older versions as artefacts of the latest build.
- Build is the target that performs the build of the deliverable projects. It has a dependency on AssemblyInfo to ensure that each build has a new build number assigned.
- Test builds the test projects and runs the unit tests using the Gallio test runner. It depends on Build to ensure that the latest code is being tested.
- Package takes the output of the build process and packages it into a zip file using the MSBuild Community Tasks Zip task. I have generally found that a zip file is the easiest way to deliver systems but there are scenarios where an installer is preferable or mandated. In these cases a tool such as WiX may be employed.
.NET Assemblies support two versions, an AssemblyVersion used in binding and an AssemblyFileVersion. I assign the AssemblyVersion manually at the top of the build file. This means I can in general ignore the need for publishing policies or per-program binding configuration. I use the AssemblyFileVersion to differentiate between different builds. The generated version if produced by the Automatic configuration of the MSBuild Community Tasks which generate a build and revision number and use the same major and minor versions as that of the AssemblyVersion. This gives a build number that is the number of days since January 1 2000 and a revision number that is a scaled value indicating the time of day.
In addition to assigning the AssemblyVersion and AssemblyFileVersion metadata the build number (supplied by the build server) is also compiled into the assembly as part of the Product metadata. The zip file produced by the Package target includes the AssemblyVersion and also the build number in its filename to assist in identifying its contents relative to other builds.
When configuring the build server I have it build both the Test and Package targets. This causes the system to be built, tested and packaged into a zip file. As configured any kind of failure including a failed unit test will prevent a build package being created. This is appropriate in the scenarios I work with but in some environments you may wish to alter the process so that failed tests do not prevent a package being generated. The build server can be configured to take anything in the Package directory as a build artefact (how you configure this will depend on the build server you are using).
This script is my current baseline for a single solution, single output system. I have used variants to work with multiple solutions and with multiple outputs (distinct executables that need a subset of the projects). I may at some point discuss these (although it could take some time until I do).