Many .NET projects are built entirely within Visual Studio. This is generally the easiest method when developing. However there are a number of cases where this overhead isn't desirable. These include obvious cases like build servers where Visual Studio may not even be available. It also includes less obvious cases such as having multiple solutions to be built. Opening a solution, especially a large one, in Visual Studio is a significant overhead you generally don't want to incur just to built the latest version of a supporting project. This is where build systems come into play.
There are a number of build tool options for .NET. NAnt is one of the original tools for the platform, inspired by the Ant from the Java world. This eventually prompted Microsoft to develop their own build tool: MSBuild. As of Visual Studio 2005 the project file format is MSBuild, although solution files annoyingly continue to be a different format which MSBuild can process but which does not support the same kind of extensibility. NAnt tends to be more feature rich but the standard case MSBuild setup is easier (it's already done for you by Visual Studio) and will likely be an easier sell in many environments.
If you simply wish to build a single standalone project then having MSBuild build the standard solution file is probably good enough for you. However in more complex cases you probably want a separate build file to give you more control. For instance one project with which I am familiar has numerous solutions for the different services that make up the system. These services have message projects that are referenced by each of the other solutions. The initial solution to this problem is to check in a binary version of each message assembly into the dependency directory associated with every solution that uses those messages. This is annoying and error prone to manage.
What we want is a system whereby we can be more sophisticated with the build. It'd be nice if we could build the messages from each solution, copy them to a common location from which they can be referenced, then build everything else. It'd also be nice to be able to run the unit tests for each solution.
Over the next few posts I'm going to demonstrate a set of MSBuild scripts that can do this, leveraging as much of what Visual Studio generates for me as is reasonable. The goal of these build files is ultimately to allow a single action to build an entire series of interrelated solutions.
As a side note: Don't have circular dependencies between solutions unless you really need them. Don't ever attempt circular dependencies between projects (within or between solutions). Not for any reason.
We start with the solution structure. You have a couple of options:
- Organise the projects into directories based on their build groups. More on this below,
- Don't structure the projects around the build. You may do this because you have an existing solution that you don't want to mess with, or because you'd prefer the file structure to be mapped to the another scheme, or at least not mapped to build requirements. This requires a little more configuration but is a workable option.
- Have partial structure and use exclusions. For instance keep the main projects in the solution directory and have a Tests directory for test projects. This will likely work well right up until the moment you add a project than violates the assumptions of your exclusion. It's probably not worth it but if you have an existing structure may be your least effort option.
I'm going with a solution that is organised based on build groups. I generally identify three types:
- The standard group is the main projects of the solution. If it doesn't belong to the other groups it goes here. Build as part of the standard build process.
- The tests group contains projects specifically related to unit testing.
- The common group contains projects that are dependencies of other solutions.
What we therefore need is a build process that builds all the common group projects and makes them available to all solutions. It then builds the standard group projects for each solution and optionally builds and runs the tests.
Because common group projects are built before everything else we need to put some restrictions on them:
- Common group projects may not have dependencies on any non-common group projects (the reverse is not true).
- Dependencies between common group projects are generally acceptable within a solution but should be minimised when between solutions.
- Cyclical common group dependencies between solutions are forbidden.
Ideally we wish to avoid dependencies between solutions so that we do not have to be concerned with their build order. Where these dependencies are unavoidable cycles must be avoided. Cycles would require having multiple common groups in some or all solutions to allow building of the dependencies to occur in the necessary order. That way madness lies. One possible solution is to have a single designated framework solution that is always built first. This is likely significantly easier to manage and in many significant projects already exists in some form. Such a solution obviously cannot have any dependencies on other solutions in the project (it may share third party components as necessary of course).
My initial build script will target such a framework project. I want two options; build and clean. Build will build the entire framework, clean will remove all the non-source files. These options map to the Rebuild All and Clean All options provided by Visual Studio. I would also like the framework assemblies to be copied to a common directory to allow them to be referenced by other solutions. Initially I shall have this happen every time the solution is built, although later scripts will allow build without deploy. For this first pass I shall ignore unit tests.
My build script looks like:
1: <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
2: <PropertyGroup>
3: <DeployDirectory>$(MSBuildProjectDirectory)\..\bin</DeployDirectory>
4: </PropertyGroup>
5: <ItemGroup>
6: <ProjectFiles Include="Src\**\*.csproj"/>
7: </ItemGroup>
8: <Target Name="Clean">
9: <MSBuild Projects="@(ProjectFiles)"
10: Targets="Clean"/>
11: </Target>
12: <Target Name="Build">
13: <MSBuild Projects="@(ProjectFiles)"
14: Targets="Rebuild">
15: <Output TaskParameter="TargetOutputs" ItemName="BuildOutput"/>
16: </MSBuild>
17:
18: <Copy SourceFiles="@(BuildOutput)" DestinationFolder="$(DeployDirectory)" />
19: </Target>
20: </Project>
There are a number of elements to this file.
- On line 3 we set a property called DeployDirectory to the location where we want the files deployed to.
- On line 6 we find all the .csproj (C# project files) in the Src directory. This directory is the location for all the standard group projects (the name is not significant, just that it exists). These project files are gathered in an item group called ProjectFiles.
- Starting line 8 we have a target that cleans the solution. It does this by running MSBuild on all the project files with a target of Clean.
- Starting line 12 is the build target. This runs MSBuild on the projects with a target of Rebuild. We gather the TargetOutputs of each build into an item group called BuildOutput
- On line 18 we take the items in BuildOutput and copy then to the deploy directory
Invoking this script from MSBuild requires a number of environment variables and command line parameters. To simplify this I've created a couple of batch files. The build script looks like:
call "c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat" x86
%FrameworkDir%\%Framework35Version%\msbuild.exe /property:Configuration=Release Framework.build
The clean script looks like:
call "c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat" x86
%FrameworkDir%\%Framework35Version%\msbuild.exe /target:Clean /property:Configuration=Release Framework.build
The scripts in this case rely on the vscarsall.bat batch file installed with Visual Studio. They are also framework version specific (.NET 3.5 in this case). This is generally acceptable as the scripts will mostly be used by developers (build systems should invoke MSBuild directly) and solutions are almost always targeted to a specific .NET version. Feel free to be more sophisticated if you wish.
These scripts may be run without any parameters or interaction to build and clean the entire framework. As the build file does not contain any references to specific projects new projects in Src will be built automatically (provided they're C# projects, but the extension to other project types is trivial). As such the build scripts will not require alteration on most project additions. If there is not a file structure identifying build groups projects may need to be specified individually which requires developers to modify the build file whenever a new project is added or a project is removed. This is not a significant overhead but may be overlooked.
The result of all this is that we can build the framework and automatically copy its outputs into a common directory. Most of this Visual Studio does automatically, and the copy of outputs we could add as build events. This solution doesn't require adding a build event to every project, but this is not by itself very compelling. In future posts I will expand on the above to produce a set of build scripts that provide more of the capabilities described above, including building of multiple solutions and running of unit tests.