Gotcha: XML include processing in CruiseControl.NET
Release 1.4 of CruiseControl.NET includes a new Configuration Preprocessor. The great thing is that this provides a C preprocessor-like macro syntax, so projects can be made using relatively powerful templates.
The bad thing is that it’s new, and so there are some gotcha’s laying around. I just found one:
I defined a ccnet.config file structured like this:
<cruisecontrol
xmlns:cb="urn:ccnet.config.builder"
>
<!-- Define some local configuration symbols -->
<cb:define
FOO="bar"
/>
<!-- Define a CCnet project template called "cb:PROJECT" -->
<cb:define
name="PROJECT"
>
<project
name = "$(projectName)"
>
<!--
guts go here
-->
</project>
</cb>
<!-- Use template -->
<cb:PROJECT
projectName = "My dog has fleas"
/>
</cruisecontrol>
That worked fine, and I could see how to rubber-stamp that project template to all my various continuous-integration areas and other workspaces.
But then I got clever, and decided to separate out the template from the local configuration.
<cruisecontrol
xmlns:cb="urn:ccnet.config.builder"
>
<!-- Define some local configuration symbols -->
<cb:define
FOO="bar"
/>
<!-- Include template(s) file -->
<cb:include
href = "project-template.xml"
/>
<!-- Use template -->
<cb:PROJECT projectName = "My dog has fleas" />
</cruisecontrol>
By unfortunate coincidence, I had to reboot my laptop at this point — I kicked the power strip under the desk, removing power to the docking station. The Lenovo I’m using (client provided) doesn’t handle this at all, so it was restart time. When I came back up, the CruiseControl.NET service wouldn’t start.
Debugging is simple and easy with the “ccnet.exe” command line utility. Just run
ccnet.exe -config:ccnet.config -validate
The error, though, was incomprehensible:
C:\Program Files\CruiseControl.NET\server>ccnet -config:ccnet.config -validate CruiseControl.NET Server 1.4.0.3400 -- .NET Continuous Integration Server Copyright c 2008 ThoughtWorks Inc. All Rights Reserved. .NET Runtime Version: 2.0.50727.1433 Image Runtime Version: v2.0.50727 OS Version: Microsoft Windows NT 5.1.2600 Service Pack 3 Server locale: e n-US [CCNet Server:DEBUG] The trace level is currently set to debug. This will cause CCNet to log at the most verbose level, which is useful for setting up or debug ging the server. Once your server is running smoothly, we recommend changing th is setting in C:\Program Files\CruiseControl.NET\server\ccnet.exe.config to a lo wer level. [CCNet Server:INFO] Reading configuration file "C:\Program Files\CruiseControl.NET\server\ccnet.config" [CCNet Server:ERROR] Exception: The configuration file contains invalid xml: C:\Program Files\CruiseControl.NET\server\ccnet.config ---------- ThoughtWorks.CruiseControl.Core.Config.ConfigurationException: The configuration file contains invalid xml: C:\Program Files\CruiseControl.NET\server\ccnet.config ---> System.Xml.XmlException: 'cb' is an undeclared namespace. Line 1, position 2. at System.Xml.XmlTextReaderImpl.Throw(Exception e) at System.Xml.XmlTextReaderImpl.Throw(String res, String arg, Int32 lineNo, Int32 linePos) at System.Xml.XmlTextReaderImpl.LookupNamespace(NodeData node) at System.Xml.XmlTextReaderImpl.ElementNamespaceLookup() at System.Xml.XmlTextReaderImpl.ParseAttributes() at System.Xml.XmlTextReaderImpl.ParseElement() at System.Xml.XmlTextReaderImpl.ParseDocumentContent() at System.Xml.XmlTextReaderImpl.Read() at System.Xml.XPath.XPathDocument.LoadFromReader(XmlReader reader, XmlSpace space) at System.Xml.XPath.XPathDocument..ctor(Stream stream) at ThoughtWorks.CruiseControl.Core.Config.Preprocessor.ConfigPreprocessorEnvironment.push_include(String href) at ThoughtWorks.CruiseControl.Core.Config.Preprocessor.ConfigPreprocessor.PreProcess(XmlReader input, XmlWriter output, PreprocessorUrlResolver resolver, Uri input_uri) at ThoughtWorks.CruiseControl.Core.Config.DefaultConfigurationFileLoader.CreateXmlValidatingLoader(FileInfo configFile) at ThoughtWorks.CruiseControl.Core.Config.DefaultConfigurationFileLoader.AttemptLoadConfiguration(FileInfo configFile) --- End of inner exception stack trace --- at ThoughtWorks.CruiseControl.Core.Config.DefaultConfigurationFileLoader.AttemptLoadConfiguration(FileInfo configFile) at ThoughtWorks.CruiseControl.Core.Config.DefaultConfigurationFileLoader.Load(FileInfo configFile) at ThoughtWorks.CruiseControl.Core.Config.FileConfigurationService.Load() at ThoughtWorks.CruiseControl.Core.Config.FileWatcherConfigurationService.Load() at ThoughtWorks.CruiseControl.Core.Config.CachingConfigurationService.Load() at ThoughtWorks.CruiseControl.Core.CruiseServer..ctor(IConfigurationService configurationService, ProjectIntegratorListFactory projectIntegratorListFactory, IProjectSerializer projectSerializer) at ThoughtWorks.CruiseControl.Core.CruiseServerFactory.Create(Boolean remote, String configFile) at ThoughtWorks.CruiseControl.Core.ConsoleRunner.Run() at ThoughtWorks.CruiseControl.Console.ConsoleMain.Main(String[] args) ----------
Even worse, the error message is presented in white text on a glaring red background, so as to be all but unreadable. Looking at it now, in retrospect (and black & white), I can see clues that I missed the first time. Specifically, the string “push_include.”
Anyway, the problem turned out to be the way the Configuration Processor deals with XML includes. It is not a C preprocessor — it includes subtrees, not text.
The answer turns out to be in the included file. References in the included file to the “cb:” XML namespace have to be accompanied in that file by some definition. Adding the same xmlns= statement does the trick:
<?xml version="1.0" encoding="UTF-8"?> <cb:define xmlns:cb="urn:ccnet.config.builder" name = "PROJECT" > <!-- blah blah --> </cb:define>
Problem solved. Now let the rubber-stamping begin!
