I’ve finally managed to completely rewrite our build script using CruiseControl.NET and MSBuild (previously we used FinalBuilder 5, but decided to leave it because of various reasons). The work itself was really interesting as far as I’ve never used MSBuild before. Well, ok, we did use it always when building .Net part of our application, but that was really behind the scenes and we have never changed anything manually in .vcproj files.
At first I was really optimistic about writing all the stuff for our build solely on CCNet, but it appeared impossible because of its poor handling of user-defined variables and lack of flexibility for custom operations. That was the point where MSBuild helped a lot (take a look at Simple MSBuild Project tutorial to see what it looks like). I will describe several situations where I had to find own solution below.
One CruiseControl.NET server for multiple projects
I guess it is a common situation when one CCNet server is used to build several different projects (e.g. made by different teams). The projects’ configurations themselves can be rather large and changing one of them in main ccnet.config file might cause problems for others. Also I think that it would be a good practice to have projects’s own configuration being versioned alongside the whole project. There are two means of including parts of configuration into the main config file: via XML’s ENTITY and via CCNet’s preprocessor. I prefer the latter one for two reasons: the code to include external files is much more readable and files included this way are automatically watched for changes (e.g. if you change external part the configuration automatically reloads). So in our case the ccnet.config files looks simply like:
<!DOCTYPE cruisecontrol> <cruisecontrol xmlns:cb="urn:ccnet.config.builder"> <cb:include href="path_to_project_1\project.config" xmlns:cb="urn:ccnet.config.builder" /> <cb:include href="path_to_project_2\project.config" xmlns:cb="urn:ccnet.config.builder" /> </cruisecontrol>
It’s really simple and effective.
Control flow in CruiseControl.NET
The latest stable version of CCNet (the 1.5 at this moment) I had did not have any means to separate control flow depending on dynamic values. It was announced to be the part of v1.6 release, but I did not want to risk with unstable version, so I had to download the CCNet Conditional plugin. The precompiled version did not work for me as far as CCNet did not see the plugin after copying to the rest of the CCNet server files. So I had to download the sources of the plugin and compile it on my work PC. The newly build plugin worked like a charm… until you need to use the logical operators: “or”/”and”. It seems to be a mistake in plugin documentation as far as it stated that the appropriate conditional tasks to be used in config file were called “orConditions”/”andConditions” respectively, but config file validation always failed on this element. After couple of hours spent in vain I’ve made a stupid thing: I removed the plural ending of the keyword, and it worked! So the typical usage of the conditionals in CCNet project would look like:
<conditional> <conditions> <orCondition> <conditions> <compareCondition value1="$[Distribution|None]" evaluation="equal" value2="CD" /> <compareCondition value1="$[Distribution|None]" evaluation="equal" value2="Both" /> </conditions> </orCondition> </conditions> <tasks> <!-- primary tasks --> </tasks> <elseTasks> <!-- alternative tasks --> </elseTasks> </conditional>
The long path CCNet Build Publisher problem
I guess it is not a common problem, but we had to handle this case, Our InstallAware installer script generated the uncompressed package that contained .NET redistributables and when we tried to copy it to the server it appeared to have really long path names (e.g. they exceeded the limit of 260 characters) that caused build task to fail. We’ve tried several solutions (like using xcopy, or performing the copy operation by MSBuild script) but it looks like most of them have the same issue. So the last way we had to resort to is to use robocopy. It is an official tool that is a part of Windows OS since Vista (i.e. Server 2008 and Win7 already have it) but for Server 2003 we had to install additional Windows Resource Kit package. Therefore there are couple of thing I suggest to pay attention to. First use /NP switch because otherwise your build log will be full of worthless copy progress lines. Second robocopy uses exit codes that are usually treated as errors (like 1, 2 etc), so you should ignore them (bad idea), or read robocopy docs and add exit code exceptions.
MSBuild absolutely necessary plugin
I cannot imagine having my work done without MSBuildTasks project. It has lots of cool stuff that is not really part of MSBuild, but turns it into the really powerful tool. Just download and install it and add following line in your MSBuild *proj file:
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
Altering properties by conditions in MSBuild
One of the things that I needed to do was to generate the DefineConstants property for our project configuration depending on CCNet dynamic values. Moreover I needed to alter them according to user input. E.g. we have A/B switch and flag C. If switch is set to A then PROP_FOO should be set, if switch SWITCH_AB is set to B then PROP_BAR, and if FLAG_C is on, then PROP_BLAHBLAHBLAH should be added. It is done via the PropertyGroup item introduced in MSBuild v3.5:
<PropertyGroup Condition="'$(SWITCH_AB)' == 'A'"> <DefineConstants>PROP_FOO</DefineConstants> </PropertyGroup> <PropertyGroup Condition="'$(SWITCH_AB)' == 'B'"> <DefineConstants>PROP_BAR</DefineConstants> </PropertyGroup> <PropertyGroup Condition="'$(FLAG_C)' == 'yes'"> <DefineConstants>$(DefineConstants);PROP_BLAHBLAHBLAH</DefineConstants> </PropertyGroup>
Moreover with PropertyGroup the CreateProperty task is not needed anymore (in my cases at least).
Autogenerating AssemblyInfo.cs with version info passed from CCNet
As easy as pie:
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" /> <Target Name="BeforeBuild"> <AssemblyInfo Condition="'$(CCNetLabel)' != ''" CodeLanguage="CS" OutputFile="Properties\AssemblyInfo.cs" AssemblyTitle="Software Name" AssemblyCompany="My Company" AssemblyProduct="Software Title" AssemblyDescription="Software Description" AssemblyCopyright="Copyright" ComVisible="false" Guid="xxxx-whatever" AssemblyVersion="$(CCNetLabel)" AssemblyFileVersion="$(CCNetLabel)" /> </Target>
Passing values to MSBuild task
It appears that only pre-defined CCNet values values (like $CCNetLabel) are passed to MSBuld task automatically. I wonder if there is a best solution, but if I needed to pass dynamic or custom values to MSBuild project, did it like this:
<msbuild> <executable>C:\WINDOWS\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable> <projectFile>MyReleaseScripts\so_something.proj</projectFile> <buildArgs>/noconsolelogger /p:Distribution=0 <!-- static value --> /p:VersionShort=$(ProjectVersion) <!-- custom value defined with cb:define --> /p:BuildKind=$[BuildKind|nightly] <!-- dynamic value --> /p:CustomFlag=$[CustomFlag|yes] <!-- dynamic value --> </buildArgs> <targets>$[Project|None]</targets> <timeout>3600</timeout> <logger>$(CCNetPath)\server\ThoughtWorks.CruiseControl.MsBuild.dll</logger> </msbuild>
The control flow codes in .Net was really cool. Thanks Sergii for posting it. We have put together many tutorials as well that may benefit your readers at https://www.linkedin.com/company/firebox-training
Great article!!! Can I please know currently i am using ccnet 184.108.40.206 and i am trying to get $(CCNetLabel) out into Msbuild script but when i run on the server it doesn't have any value in $(CCNetLabel). I am new to ccnet as i have used teamcity before but have used ccnet. In order to get the labeller into my msbuild can i know if i need to do any installation. Thnx Hits
Great article! Just wondering how you get the project configs pulled out from the SCM. They must initially be pulled out manually, right? Because the .config file referred in the ccnet.config's "<cb: include href=..." must be present before that project itself can be pulled out from the SCM by ccnet
How are you getting around the problem where ccnet tries to checkout (2nd+ time) to the non-empty folder hosting the projectX.config, but throws an exception? It expects the folder to be emty. Is there a way to configure ccnet's call to svn?
Hm... I have never tried this approach with SVN as far as I use Mercurial and thus I didn't have any issues. Putting the project config file to some sibling directory to the main sources might be a quick solution if it is possible in your case.