The sources described in this post can be downloaded here.
This post follows up from my previous post, Configuring Projects on Multiple Instances of Cruise Control .Net. In one of the comments to this post, Elad asked if it was possible to reuse configuration files on the same instance of Cruise Control.Net. This would make sense in scenarios where you want to keep different branches of the same project integrated on the same machine without re-defining all the configuration for each. While this makes perfect sense, I couldn’t find any way to do this directly, so I tried to come up with a workaround that allows this. Please bear this in mind while you read the rest of this post. My knowledge of XML in general and Cruise Control configurations in particular is neither all-encompassing nor flawless in its brilliance, so there are probably, oh, a few million holes you could poke into this method. That said, I’m always open for comments, so if you have a better way, please share it 🙂
In order to allow some sort of parameterization of the configurations, we’re going to introduce a middle man. This will take the form of an asp.net page, and will take an XML file containing details specific to the build, and transform it (via XSLT) into a full project element.
The XML files we will use are fairly simple things – this is an important point, because these are the files that we’ll be creating most often. The files will be in the following format:
1: <?xml version="1.0" ?>
2: <project name="name-release" workingdir="workingdir for release">
3: <svn url="svnurl" username="username" password="password" />
4: <msbuild script="buildscript for release" arguments="buildargs for release" />
5: </project>
Note that we’ve tagged the name of the project with -release to indicate what sort of build this will be. The workingdir attribute will be used for the project working directory, and the svn checkout directory (that’s why there’s no checkout folder described in the svn element). The attributes for the svn and msbuild elements will be replaced into the equivalent elements in the project configuration.
The XSLT will take care of the boilerplate stuff, and stick in any build-specific information:
1: <?xml version="1.0" encoding="utf-8"?>
2: <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3: <xsl:output method="text" media-type="text/plain" version="1.0" encoding="utf-8" indent="yes" omit-xml-declaration="yes" />
4:
5: <xsl:variable name="localprojectname"><xsl:value-of select="/project/@name" />-<![CDATA[&]]>serverDescription;</xsl:variable>
6:
7: <xsl:template match="/">
8: <xsl:apply-templates />
9: </xsl:template>
10:
11: <xsl:template match="project">
12: <project>
13: <xsl:attribute name="name"><xsl:copy-of select="$localprojectname" /></xsl:attribute>
14:
15: <workingDirectory><xsl:value-of select="@workingdir" /></workingDirectory>
16: <webURL>http://<![CDATA[&]]>serverAddress;/ccn et/server/local/project/<xsl:copy-of select="$localprojectname" />/ViewLatestBuildReport.aspx</webURL>
17: <triggers>
18: <intervalTrigger name="continuous" seconds="30" buildCondition="IfModificationExists"/>
19: </triggers>
20:
21: <xsl:apply-templates />
22:
23: <publishers>
24: <merge>
25: <files>
26: <file>C:\Projects\Reports\<xsl:copy-of select="$localprojectname" />.NUnit-result.xml</file>
27: <file>C:\Projects\Reports\<xsl:copy-of select="$localprojectname" />.FxCop-result.xml</file>
28: <file>C:\Projects\Reports\<xsl:copy-of select="$localprojectname" />.NCover-result.xml</file>
29: <file>C:\Projects\Reports\<xsl:copy-of select="$localprojectname" />.NCoverageReport.xml</file>
30: </files>
31: </merge>
32: <xmllogger>
33: <xsl:attribute name="logDir">C:\Program Files\CruiseControl.NET\Logs\<xsl:copy-of select="$localprojectname" /></xsl:attribute>
34: </xmllogger>
35: </publishers>
36: </project>
37: </xsl:template>
38:
39: <xsl:template match="msbuild">
40: <msbuild>
41: <executable>C:\WINDOWS\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
42: <projectFile><xsl:value-of select="@script" /></projectFile>
43: <buildArgs><xsl:value-of select="@arguments" /></buildArgs>
44: <targets>Build</targets>
45: <logger>ThoughtWorks.CruiseControl.MsBuild.XmlLogger,C:\Program Files\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MsBuild.dll</logger>
46: </msbuild>
47: </xsl:template>
48:
49: <xsl:template match="svn">
50: <sourcecontrol type="svn">
51: <workingDirectory><xsl:value-of select="../@workingdir" /></workingDirectory>
52: <trunkUrl><xsl:value-of select="@url" /></trunkUrl>
53: <executable>c:\build_tools\svn\svn.exe</executable>
54: <username><xsl:value-of select="@username" /></username>
55: <password><xsl:value-of select="@password" /></password>
56: </sourcecontrol>
57: </xsl:template>
58:
59: </xsl:stylesheet>
As you can see we’re still using the &serverDescription; and &serverAddress; entities which we defined in the previous post. These must still be defined in the main ccnet.config file of each build server. The entities defining the projects, however, should now reference our XSLT converter, rather than the file on the file system:
1: <!DOCTYPE cruisecontrol[
2: <!ENTITY serverDescription "WinXP">
3: <!ENTITY serverAddress "xpmachine.mybuildservers.com">
4: <!ENTITY myprojectrelease SYSTEM "http://localhost:2093/Default.aspx?source=release.xml">
5: <!ENTITY myprojectdev SYSTEM "http://localhost:2093/Default.aspx?source=debug.xml">
6: ]>
7: <cruisecontrol>
8: &myprojectrelease;
9: &myprojectdev;
10: </cruisecontrol>
NOTE: The url specifies port 2093 because I was using the Visual Studio web server to test this code. The port should be whatever your asp.net server uses.
Now, the project configurations specify a url, passing the name of the configuration file to use as a parameter. The url should point to an asp.net page containing the following code; this will act as a converter. Note that the following assumes that the configuration files will be present in its own folder, not in the project folder.
1: <%@ Import Namespace="System.Xml" %>
2: <%@ Import Namespace="System.Xml.Xsl" %>
3: <%@ Import Namespace="System.Xml.XPath" %>
4: <script language="C#" runat="server">
5: public void Page_Load(Object sender, EventArgs e)
6: {
7: string xmlPath = Server.MapPath(Request.Params["source"]);
8: string xslPath = Server.MapPath("configuration-template.xsl");
9:
10: XPathDocument doc = new XPathDocument(xmlPath);
11: XslTransform transform = new XslTransform();
12: transform.Load(xslPath);
13:
14: StringBuilder builder = new StringBuilder();
15: XmlWriterSettings settings = new XmlWriterSettings();
16: settings.Indent = true;
17: settings.CheckCharacters = false;
18: settings.NewLineHandling = NewLineHandling.None;
19: settings.OmitXmlDeclaration = true;
20: settings.Encoding = Encoding.UTF8;
21:
22: using (XmlWriter writer = XmlTextWriter.Create(builder, settings))
23: transform.Transform(doc, null, writer);
24:
25: builder.Replace("&serverDescription;", "&serverDescription;");
26: builder.Replace("&serverAddress;", "&serverAddress;");
27:
28: Response.Write(builder.ToString());
29: }
30: </script>
NOTE: The builder.Replace(…) statements at the end are there because I couldn’t find a way to force the writer not to escape the & character; It kept getting pushed out as &
If everything is ok, Cruise Control should now see the file as follows:
1: <cruisecontrol>
2: <project name="name-release-WinXP">
3: <workingDirectory>workingdir for release</workingDirectory>
4: <webURL>http://xpmachine.mybuildservers.com/ccnet/server/local/project/name-release-WinXP/ViewLatestBuildReport.aspx</webURL>
5: <triggers>
6: <intervalTrigger name="continuous" seconds="30" buildCondition="IfModificationExists" />
7: </triggers>
8: <sourcecontrol type="svn">
9: <workingDirectory>workingdir for release</workingDirectory>
10: <trunkUrl>svnurl</trunkUrl>
11: <executable>c:\build_tools\svn\svn.exe</executable>
12: <username>username</username>
13: <password>password</password>
14: </sourcecontrol>
15: <msbuild>
16: <executable>C:\WINDOWS\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
17: <projectFile>buildscript for release</projectFile>
18: <buildArgs>buildargs for release</buildArgs>
19: <targets>Build</targets>
20: <logger>ThoughtWorks.CruiseControl.MsBuild.XmlLogger,C:\Program Files\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MsBuild.dll</logger>
21: </msbuild>
22: <publishers>
23: <merge>
24: <files>
25: <file>C:\Projects\Reports\name-release-WinXP.NUnit-result.xml</file>
26: <file>C:\Projects\Reports\name-release-WinXP.FxCop-result.xml</file>
27: <file>C:\Projects\Reports\name-release-WinXP.NCover-result.xml</file>
28: <file>C:\Projects\Reports\name-release-WinXP.NCoverageReport.xml</file>
29: </files>
30: </merge>
31: <xmllogger logDir="C:\Program Files\CruiseControl.NET\Logs\name-release-WinXP" />
32: </publishers>
33: </project>
34: <project name="name-dev-WinXP">
35: <workingDirectory>workingdir for dev</workingDirectory>
36: <webURL>http://xpmachine.mybuildservers.com/ccnet/server/local/project/name-dev-WinXP/ViewLatestBuildReport.aspx</webURL>
37: <triggers>
38: <intervalTrigger name="continuous" seconds="30" buildCondition="IfModificationExists" />
39: </triggers>
40: <sourcecontrol type="svn">
41: <workingDirectory>workingdir for dev</workingDirectory>
42: <trunkUrl>svnurl</trunkUrl>
43: <executable>c:\build_tools\svn\svn.exe</executable>
44: <username>username</username>
45: <password>password</password>
46: </sourcecontrol>
47: <msbuild>
48: <executable>C:\WINDOWS\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
49: <projectFile>buildscript for release</projectFile>
50: <buildArgs>buildargs for dev</buildArgs>
51: <targets>Build</targets>
52: <logger>ThoughtWorks.CruiseControl.MsBuild.XmlLogger,C:\Program Files\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MsBuild.dll</logger>
53: </msbuild>
54: <publishers>
55: <merge>
56: <files>
57: <file>C:\Projects\Reports\name-dev-WinXP.NUnit-result.xml</file>
58: <file>C:\Projects\Reports\name-dev-WinXP.FxCop-result.xml</file>
59: <file>C:\Projects\Reports\name-dev-WinXP.NCover-result.xml</file>
60: <file>C:\Projects\Reports\name-dev-WinXP.NCoverageReport.xml</file>
61: </files>
62: </merge>
63: <xmllogger logDir="C:\Program Files\CruiseControl.NET\Logs\name-dev-WinXP" />
64: </publishers>
65: </project>
66: </cruisecontrol>
Phew. Quite frankly it’s a rather roundabout way to get this done. If anyone has a simpler or cleaner way of achieving the same result, I’d be extremely happy to know how 🙂
Thanks! This is very interesting. I think I’ll use this technology with some tailoring to our specific needs. I’ll let you know when I get some results.
Glad it helped! Let me know if there are any issues, or if you have any suggestions to add to it 🙂 Thanks for your feedback!
wow, very interesting! i agree that it is quite roundabout; it’s a shame that seems cruisecontrol.net doesn’t support some kind of sharing of options between similar projects (or multiple branches of the same project).
This is much easier accomplished by using the Configuration Preprocessor, to define defaults that can be reused, and/or to create nested s that define common project properties and overwrite with configuration specific changes.
See http://confluence.public.thoughtworks.org/display/CCNET/Configuration+Preprocessor
Good tip – thanks Nick 🙂