Alan Dean

CTO, Developer, Agile Practitioner

Photograph of Alan Dean

Showing posts with label Build. Show all posts
Showing posts with label Build. Show all posts

Sunday, February 20, 2011

How to conditionally skip compilation

As I mentioned in my last post, I was having problems getting an MVC web application to play nice with my framework targeting builds. Specifically, my CI server would fall over if I tried to build using Framework 3.5 when the web application was using 4.0 (due to 4.0-specific web.config settings that 3.5 does not recognise). I managed to spend a large portion of the afternoon searching for an answer until I had one of those “eureka” moments. My objective was to simply not build the web application unless the framework was 4.0 (the same would apply to any project that is framework version-specific). I tried putting a conditional on the project element. No Joy. I tried a whole bunch of other things and it was starting to look like I would have to manually configure each project to be built – very nasty, not at all what I want.

My “eureka” moment happened as I was gazing forlornly at a project file. My eye alighted upon an attribute of DefaultTargets="Build" on the project element. Build? I thought… I’ve not seen a target called that. A quick check confirmed my suspicion. The “Build” target is effectively inherited. In a flash I thought I wonder if I can intercept this? I changed the attribute to DefaultTargets="Conditional" and then added the following target and all is well again in Narnia:

<Target Name="Conditional">
<CallTarget Targets="Build" Condition=" '$(TargetFrameworkVersion)' == 'v4.0' " />
</Target>

Framework version targeting with MSBUILD

I faced a question this weekend: how best should I support conditional compilation by framework? The proximate reason for asking this was a need to use BigInteger in the Cavity project in order to be able to support 128-bit base-36 notation with a minimum of fuss. This is a new value type in Framework 4.0 and up until now Framework 3.5 have been the target for Cavity assemblies. As 3.5 is going to be around for a good while yet (heck, there is still plenty of 2.0 in production today) I didn’t want to orphan support for that version. I also didn’t want to add complexity to my build or maintenance activities. This ruled out brute force options such as creating a branch for 3.5 or creating a new solution and project files for one or other of the framework versions.

A search yielded a varied collection of advice. Some was plainly potty but a couple of StackOverflow answers [1] [2] looked promising. Taking the answers and combining a bit of common sense, I put together a quick spike to verify that I had the gist of it. Here is what I learnt:

  1. The best way to control targeting is to use property parameters with a build file. Here is a .bat file to build release versions targeting each major framework version:
    MSBUILD build.xml /p:Configuration=Release /p:TargetFrameworkVersion=v2.0
    MSBUILD build.xml /p:Configuration=Release /p:TargetFrameworkVersion=v3.5
    MSBUILD build.xml /p:Configuration=Release /p:TargetFrameworkVersion=v4.0

  2. You don’t need to do anything in the build.xml or the .sln file for this to work. You do, however, need to do a little work in the .csproj files:
    • First, you need to decide which framework version you want to work with in Visual Studio (typically, this will be the latest version) and configure TargetFrameworkVersion accordingly:

      <TargetFrameworkVersion Condition=" '$(TargetFrameworkVersion)' == '' ">v4.0</TargetFrameworkVersion>

    • To make the build output obvious, set the OutputPath to use properties:

      <OutputPath>bin\$(Configuration) $(TargetFrameworkVersion)\</OutputPath>

    • Next, you should set up some conditional property groups:
      <PropertyGroup Condition=" '$(TargetFrameworkVersion)' == 'v2.0' ">
          <DefineConstants>NET20</DefineConstants>
          <TargetFrameworkVersionNumber>2.0</TargetFrameworkVersionNumber>
      </PropertyGroup>
      <PropertyGroup Condition=" '$(TargetFrameworkVersion)' == 'v3.5' ">
          <DefineConstants>NET35</DefineConstants>
          <TargetFrameworkVersionNumber>3.5</TargetFrameworkVersionNumber>
      </PropertyGroup>
      <PropertyGroup Condition=" '$(TargetFrameworkVersion)' == 'v4.0' ">
          <DefineConstants>NET40</DefineConstants>
          <TargetFrameworkVersionNumber>4.0</TargetFrameworkVersionNumber>
      </PropertyGroup>

    • This allows you to configure framework-specific assembly references, such as System.Core and Microsoft.CSharp:

      <Reference Include="System.Core" Condition=" '$(TargetFrameworkVersionNumber)' >= '3.5' " />
      <Reference Include="Microsoft.CSharp" Condition=" '$(TargetFrameworkVersionNumber)' >= '4.0' " />

    • You should now be able to target framework versions in a straightforward manner by running the batch file above.

  3. If you want to conditionally include classes, simply apply a framework condition:
    <Compile Include="Class20.cs" Condition=" '$(TargetFrameworkVersion)' == 'v2.0' " />

  4. I also defined constants above to allow conditional compilation at code level:
    namespace Example
    {
        public sealed class Class1
        {
    #if NET20
            public void Net20()
            {
            }
    #endif
    
    #if NET35
            public void Net35()
            {
            }
    #endif
    
    #if NET40
            public void Net40()
            {
            }
    #endif
        }
    }



The only pain I have really encountered so far is getting MVC web applications to play nicely.

Saturday, June 26, 2010

Run StyleCop on every build

I first started using StyleCop during a couple of projects with Microsoft Services when it was called Source Analysis and I’m a big fan because it helps makes code consistently formatted across a codebase. In order to have StyleCop run on every build, simply open the project file in the text editor of your choice (or you can unload the project from within the solution and then right-click to edit within Visual Studio) and add the Microsoft.StyleCop.targets import (I normally add it immediately after the Microsoft.CSharp.targets import):

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    ...
    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
    <Import Project="$(MSBuildExtensionsPath)\Microsoft\StyleCop\v4.3\Microsoft.StyleCop.targets" />
    ...
</Project>

P.S. A plea to Microsoft: can we have the standard project and class templates pass StyleCop by default please?

Deploying to IIS7 from MSBuild

Here is an example of how to configure deployment of a web application on a development machine using MSBuild with the Extension Pack:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Run" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">

    <Import Project="$(MSBuildProjectDirectory)\lib\trove\Framework\v2.0\MSBuild.Community.Tasks.Targets" />
    <Import Project="$(MSBuildProjectDirectory)\lib\trove\Framework\v3.5\MSBuild.ExtensionPack.tasks" />

    <Target Name="Run">
        <CallTarget Targets="Clean" />
        <CallTarget Targets="Build" />
        <CallTarget Targets="Deploy" Condition="'$(registry:HKEY_LOCAL_MACHINE\Software\Microsoft\InetStp@MajorVersion)'=='7'" />
    </Target>

    <Target Name="Clean">
        <MSBuild
            Projects="$(MSBuildProjectDirectory)\src\Example.sln"
            Targets="Clean"
            Properties="Configuration=$(Configuration)"
            />
    </Target>

    <Target Name="Build">
        <MSBuild
            Projects="$(MSBuildProjectDirectory)\src\Example.sln"
            Targets="Rebuild"
            Properties="Configuration=$(Configuration)">
            <Output
                TaskParameter="TargetOutputs"
                ItemName="CodeAssemblies"
                />
        </MSBuild>
    </Target>

    <PropertyGroup>
        <WebApplicationName>www.example.net</WebApplicationName>
        <WebApplicationPath>$(MSBuildProjectDirectory)\src\Web Applications\Example</WebApplicationPath>
    </PropertyGroup>

    <Target Name="Deploy">
        <MSBuild.ExtensionPack.Web.Iis7Website
            TaskAction="CheckExists" 
            Name="$(WebApplicationName)">
            <Output
                TaskParameter="Exists"
                PropertyName="WebApplicationExists"
                />
        </MSBuild.ExtensionPack.Web.Iis7Website>
        <MSBuild.ExtensionPack.Web.Iis7Website
            TaskAction="Create"
            Name="$(WebApplicationName)"
            Path="$(WebApplicationPath)"
            Port="80"
            AppPool="ASP.NET v4.0"
            Condition="'$(WebApplicationExists)'=='False'"
            />
        <MSBuild.ExtensionPack.Web.Iis7Binding
            TaskAction="Remove"
            Name="$(WebApplicationName)"
            BindingInformation="*:80:"
            BindingProtocol="http"
            />
        <MSBuild.ExtensionPack.Web.Iis7Binding
            TaskAction="Add"
            Name="$(WebApplicationName)"
            BindingInformation="127.0.0.127:80:$(WebApplicationName)"
            BindingProtocol="http"
            Condition="'$(WebApplicationExists)'=='False'"
            />
        <MSBuild.Community.Tasks.Sleep Milliseconds="3000" />
        <MSBuild.ExtensionPack.Web.Iis7Website
            TaskAction="Stop"
            Name="$(WebApplicationName)"
            />
        <MSBuild.Community.Tasks.Sleep Milliseconds="3000" />
        <MSBuild.ExtensionPack.Web.Iis7Website
            TaskAction="Start"
            Name="$(WebApplicationName)"
            />
    </Target>

</Project>

Friday, June 25, 2010

Consistent Assembly Versioning

Personally, I like to have consistent versioning applied to all assemblies from the same build. Doing this manually is a PITA so I version from my build file and I have a pattern which I apply to achieve this. I typically use subversion for my source control and I will use the Cavity project as an example:

Subversion project structure

I build from the trunk folder:

Subversion trunk folder

I have a batch file for each ‘potted’ configuration I want and, of course, the MSBuild file. Here is the release batch file:

MSBUILD build.xml /p:Configuration=Release
PAUSE

In order to apply consistent versioning, I want to emit a Build.cs file and then link that to each project. If I want this to be a static number, I can simply configure the version directly and use AssemblyInfo task from the MSBuild Community Tasks Project:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Run" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
    <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />

    <PropertyGroup>
        <Configuration Condition="'$(Configuration)'==''">Release</Configuration>
        <Version Condition="'$(Version)'==''">1.2.3.4</Version>
    </PropertyGroup>

    <Target Name="Run">
        <CallTarget Targets="Clean" />
        <CallTarget Targets="Build" />
    </Target>

    <Target Name="Clean">
        <MSBuild
            Projects="$(MSBuildProjectDirectory)\src\Cavity.sln"
            Targets="Clean"
            Properties="Configuration=$(Configuration)"
        />
    </Target>

    <Target Name="Versioning">
        <AssemblyInfo
            CodeLanguage="CS"
            OutputFile="$(MSBuildProjectDirectory)\src\Build.cs"
            AssemblyVersion="$(Version)"
            AssemblyFileVersion="$(Version)"
            AssemblyInformationalVersion="$(Version)"
            />
    </Target>

    <Target Name="Build" DependsOnTargets="Versioning">
        <MSBuild
            Projects="$(MSBuildProjectDirectory)\src\Cavity.sln"
            Targets="Rebuild"
            Properties="Configuration=$(Configuration)">
            <Output
                TaskParameter="TargetOutputs"
                ItemName="CodeAssemblies"
                />
        </MSBuild>
    </Target>

</Project>

This will emit the following Build.cs:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.1
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: AssemblyVersion("1.2.3.4")]
[assembly: AssemblyFileVersion("1.2.3.4")]
[assembly: AssemblyInformationalVersion("1.2.3.4")]

However, I normally use the subversion Revision number as the build number to be able to identify what was built. To do this you will need to install the CollabNet Subversion client in order to be able to query the subversion repository. Once installed, you can then use the following build file to emit a dynamic version number by utilising the SvnVersion task:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Run" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
    <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />

    <PropertyGroup>
        <Configuration Condition="'$(Configuration)'==''">Release</Configuration>
        <Version Condition="'$(Version)'==''">1.2.3</Version>
        <Revision>0</Revision>
    </PropertyGroup>

    <Target Name="Run">
        <CallTarget Targets="Clean" />
        <CallTarget Targets="Build" />
    </Target>

    <Target Name="Clean">
        <MSBuild
            Projects="$(MSBuildProjectDirectory)\src\Cavity.sln"
            Targets="Clean"
            Properties="Configuration=$(Configuration)"
            />
    </Target>

    <Target Name="Versioning">
        <SvnVersion LocalPath=".">
            <Output TaskParameter="Revision" PropertyName="Revision" />
        </SvnVersion>
        <AssemblyInfo
            CodeLanguage="CS"
            OutputFile="$(MSBuildProjectDirectory)\src\Build.cs"
            AssemblyVersion="$(Version).$(Revision)"
            AssemblyFileVersion="$(Version).$(Revision)"
            AssemblyInformationalVersion="$(Version).$(Revision)"
            />
    </Target>

    <Target Name="Build" DependsOnTargets="Versioning">
        <MSBuild
            Projects="$(MSBuildProjectDirectory)\src\Cavity.sln"
            Targets="Rebuild"
            Properties="Configuration=$(Configuration)">
            <Output
                TaskParameter="TargetOutputs"
                ItemName="CodeAssemblies"
                />
        </MSBuild>
    </Target>

</Project>