Archive for April, 2008

Automating the build with MSBuild (part two)

Sunday, April 20th, 2008

Automating the build with MSBuild

If you missed part one please check it out and then come back here. You can also download the sample code from CodePlex.

Our next job is to get MSBuild to copy the files needed for deployment into a new directory. This is effectively the same as using the Publish Web Site option in Visual Studio. However, I couldn’t find any way to do that via MSBuild - if anyone knows how, please leave a comment. UPDATE I found a way to do this in the end, check out the post How to publish a web site with MSBuild.

Instead I’ve used the Copy task, and defined a couple of ItemGroups, one for the aspx pages and another for the contents of the bin folder. The XML below shows this.

<Target Name=”Publish”>
  <RemoveDir Directories=”Output” ContinueOnError=”true” />
  <ItemGroup>
    <Pages Include=”**\*.aspx” />
    <Binaries Include=”bin\*.dll” />
  </ItemGroup>
  <Copy SourceFiles =”@(Pages)” DestinationFolder=”Output\%(RecursiveDir)” />
  <Copy SourceFiles =”@(Binaries)” DestinationFolder=”Output\bin” />
  <Copy SourceFiles =”Web.config” DestinationFolder=”Output” />
</Target>

Along with the web.config file, these files are all that are needed for our simple website to run.

This new target introduces some more features of MSBuild. First off the RemoveDir task runs with the ContinueOnError attribute set to true. The effect of this is that, should we try to remove a directory that does not exist, the resulting error will not stop the script. This attribute is actually available for all tasks and defaults to false.

When specifying the Pages ItemGroup, using “**\*.aspx” allows us to pick up all aspx files in the project, rather than just those in the root. This is important as our web site has an Admin folder containing an aspx.

Having defined the ItemGroups it is quite simple to use the Copy task to place them in the Output folder. We do not need to explicitly create the folder either - the act of copying into it will do so.

In order to preserve the directory structure of the web site, the pages are copied using the %(RecursiveDir) syntax. Had this not been used then all pages would have been placed in the root of the Output folder.

To give the new target a quick spin, execute the following command

msbuild Build.xml /t:Publish

and you should find the Output folder populated with the binaries, aspx pages and the web.config. The site is almost ready to be deployed, however we still need to copy the environment-specific settings over. Let’s tackle that now.

As you may recall from part one, in addition to the web.config file, there are live.config and test.config files. Whilst web.config contains all settings for the web site, live.config and test.config contain only those settings that are different for that environment. It is then the job of the build script to copy the correct values over.

To achieve our goal, the general plan is to read settings out of one file, using an XPath query, and then write them back to another file, using the same XPath. A quick scan of the built-in tasks didn’t find anything useful, however the MSBuild Community Tasks Project came to the rescue. Having downloaded and installed the extra tasks they provide, we now have access to XmlRead and XmlUpdate tasks. Excellent!

Before diving in and using them, make sure the following XML is inserted below the root Project node

<Import Project=”C:\Program Files\MSBuild\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets”/>

This ensures that MSBuild knows about the extra tasks. We also need some intermediary variables to store the settings between reading and writing. MSBuild allows a script to declare a PropertyGroup, which can then contain a bunch of properties. The following XML illustrates this

<PropertyGroup>
  <Environment>Test</Environment>
  <CompilationDebug />
  <CustomErrorsMode />
  <ContentEditorsEmail />
  <AdministratorsEmail />
</PropertyGroup>

Note that a property has also been added for indicating which environment we are working with. At the moment this is hard-wired as Test but that will change later on.

The first step is to get those properties populated correctly, using the XmlRead task. This is pretty simple as can be seen below

<Target Name=”GetConfig”>
  <XmlRead XPath=”configuration/system.web/compilation/@debug”
           XmlFileName=”$(Environment).config”>
    <Output TaskParameter=”Value” PropertyName=”CompilationDebug” />
  </XmlRead>
</Target>

I’ve only shown one instance of XmlRead here, to avoid repetition. The full script contains all four. The points to make here are

  • A GetConfig target has been declared to keep all our XmlRead tasks together
  • The XmlFileName attribute has a dynamic value. Using MSBuild’s $(PropertyName) syntax it is easy to point at the correct config file dependant on the value of the Environment property
  • The Output tag is used in order to store the return value of XmlRead in the CompilationDebug property

We’re almost there now. As you may have guessed, the syntax for XmlUpdate is very similar. Again, it makes sense to group all four updates into a SetConfig target, as follows

<Target Name=”SetConfig” DependsOnTargets=”GetConfig”>
  <XmlUpdate XPath=”configuration/system.web/compilation/@debug”
             XmlFileName=”Output\web.config”
             Value=”$(CompilationDebug)” />
</Target>

The same XPath is used to find the target node in the web.config file and the Value attribute is set using the $(PropertyName) substitution technique. I also added DependsOnTargets=”GetConfig” to enforce that settings are read before they are written.

Phew! OK, we can now run our shiny new target by switching back to the command window and executing this statement

msbuild Build.xml /t:SetConfig

To test everything has worked, open up the web.config file in the Output folder and you will find that its values match those in Test.config.

At this point the script is in need of a little refactoring. It is also awkward to run all of the targets in one go. In the next part we will resolve these issues and create the deployment target. Until then please feel free to provide feedback in the comments. You can also download the script in its current state from CodePlex.

Technorati tags:

Automating the build with MSBuild

Tuesday, April 15th, 2008

Automating the build with MSBuild

One of the less well known tools that comes with the .NET framework is MSBuild. This command line utility runs XML scripts which can automate the build of a software project.

The question is, “why would I want to automate my build?”. Well, all but the most trivial of projects are likely to contain a number of build steps, for example

  • Incrementing the version number
  • Compiling source code
  • Choosing the correct settings for the deployment environment (e.g. test, live)
  • Including third party dependencies in the install package

As such, producing the build by hand can be a time-consuming and error-prone process. The initial effort invested in creating a build script will be more than offset by that gained from automation. This is especially true if the manual process often went wrong and either had to be repeated or fixed.

OK, at this point hopefully you are sold on the benefits of build automation and are champing at the bit to write your first script. Let’s take a look at our sample project, which is an ASP .NET web site.

Project structure

Notice that, in addition to the web.config file, there are live.config and test.config files. The idea here is that whilst web.config contains all settings for the web site, live.config and test.config contain only those settings that are different for that deployment environment. This avoids repeating settings in each file, which can turn into a maintenance headache. However it does present a challenge in that we will need to merge the environment-specific settings into the web.config when performing the build. More on that later.

Before we start, here are the steps that constitute the build for this project

  • Clean the bin directory
  • Compile the project
  • Copy all files required for deployment into a new build output directory
  • Merge config settings
  • Deploy the build output to the correct environment

Each project will have its own build process, however for this purposes of this demo ours is fairly basic.

Right then, let’s get started with the tutorial. If you would like to follow along, the sample project is available from CodePlex. The first step is to add a new XML file to the root of the site, called Build.xml. All MSBuild scripts should contain a root Project node at the top. Having added that, our next job is to clean the bin directory, ready for another compilation to be performed. That leaves our script looking like this

<Project xmlns=”http://schemas.microsoft.com/developer/msbuild/2003″>

  <Target Name=”Clean”>
    <ItemGroup>
      <BinFiles Include=”bin\*.*” />
    </ItemGroup>
    <Delete Files=”@(BinFiles)” />
  </Target>

</Project>

There are a number of points to make here

  • Including a namespace allows Visual Studio to provide IntelliSense for all MSBuild features
  • MSBuild scripts are largely made up of targets, which contain one or more tasks. In this case our target is called Clean and it uses the Delete task
  • ItemGroups, surprisingly, are used to define a group of items. In our case this is a list of all files in the bin folder
  • All paths, e.g. bin\*.*, are relative to the build script itself. It is a good idea to use relative paths in case you try to run the script from a different location in the future

I have to admit that I was initially unaware of the need for ItemGroup, instead expecting to simply pass “bin\*.*” to the Delete task. A post on the MSBuild Team Blog explains why.

OK, having coded our first target, let’s run it. Open a command prompt and switch directories into the folder containing your build script. Then execute this statement

msbuild Build.xml /t:Clean

If you receive an error stating that MSBuild could not be found, you will have to either fully qualify the path to it (in my case C:\WINDOWS\Microsoft.NET\Framework\v3.5), or add its location to your PATH environment variable. I recommend the latter, instructions on which can be found here.

Having run the command the bin folder should be empty. Make sure something was actually in there in the first place before giving yourself a pat on the back! As you can see, the syntax for running MSBuild is to provide the name of the build script, then the t switch with the name of the target to run.

Moving swiftly on, our next step is to compile the project. To do so we create a target called Compile, and use the MSBuild task to perform the compilation, passing it the web site’s project filename, as follows

<Target Name=”Compile” DependsOnTargets=”Clean”>
  <MSBuild Projects=”BuildDemoSite.csproj” />
</Target>

It seems a little confusing that Microsoft decided to have a task called “MSBuild”, but there you go. I’ve introduced the DependsOnTargets attribute here, which forces Clean to run before Compile does. This ensures we can’t forget to to clean the bin folder, and in fact removes the need to run Clean separately. Instead we can just call Compile, as follows

msbuild Build.xml /t:Compile

Reviewing the messages output to the command window confirms that Clean ran before Compile did.

OK, that’s it for this post. So far we have

  • Established what a build consists of
  • Decided to automate it
  • Created targets for cleaning the bin folder and compiling the project

You can download the script in its current state from CodePlex.

In part 2 we’ll create the remaining targets and refactor our script. Stay tuned and, in the meantime, please leave feedback in the comments.

Technorati tags: