|
|
|
Nick Hines and I will start work on refatoring this and testing it.
We have written the XmlLogger so will integrate that at the same time. Has any progress been made on this MSBuild plugin?
I'm starting to work with Whidbey and would love to have CruiseControl.NET support. Right now I'm blocked because NAnt's solution support doesn't understand the new MSBuild XML format. Thanks, Kirk There's a fairly useful workaround for this that Mike Swanson blogged - see:
http://blogs.msdn.com/mswanson/archive/2004/10/05/238423.aspx Full support will be coming later. Mike I've created a MSBuild logger to capture the buildresults and be able to parse them with xsl. It was inspired by one written by Wilco Bauwer and other one published by MS. To enable it use the following command line:
MSBuild.exe project.sln /v:diag /logger:Kobush.Build.Logging.XmlLogger,Kobush.MSBuild.dll;file=buildresult.xml;verbose=true You can also add following switches to remove clutter: /nologo /noconsolelogger. I've also created an xsl that will show the errors and warnings on the build page in web dashboard. Just put it in the xls subfolder nad add following line in web.config <xslfiles> section <file name="xsl\compile-msbuild.xsl" /> (or simply replace the compile.xsl file). ------- start of XmlLogger.cs --------- #region Using directives using System; using System.Globalization; using System.Collections.Generic; using System.Text; using Microsoft.Build.Utilities; using Microsoft.Build.Framework; using System.Xml; #endregion namespace Kobush.Build.Logging { /// <summary> /// Implements an XML logger for MSBuild. /// </summary> public class XmlLogger : Logger { /// <summary> /// Initializes a new instance of the <see cref="XmlLogger"/> class. /// </summary> public XmlLogger() {} private string outputPath; private XmlDocument outputDoc; private XmlElement currentElement; bool verbose = false; private void ParseParameters() { string[] propArr = this.Parameters.Split(';'); foreach (string prop in propArr) { string[] pair = prop.Split('='); switch (pair[0].ToLower().Trim()) { case "file": outputPath = pair[1]; break; case "verbose": Boolean.TryParse(pair[1], out verbose); break; default: break; } } } /// <summary> /// Initializes the logger by attaching events and parsing command line. /// </summary> /// <param name="eventSource">The event source.</param> public override void Initialize(IEventSource eventSource) { this.ParseParameters(); this.outputDoc = new XmlDocument(); this.outputDoc.AppendChild(this.outputDoc.CreateXmlDeclaration("1.0", "utf-8", "yes")); this.outputDoc.AppendChild(this.outputDoc.CreateElement(XmlLoggerElements.Build)); this.currentElement = this.outputDoc.DocumentElement; eventSource.ErrorEvent += new BuildEventHandler(eventSource_MessageHandler); eventSource.WarningEvent += new BuildEventHandler(eventSource_MessageHandler); if (Verbosity != LoggerVerbosity.Quiet) { eventSource.BuildStartedEvent += new BuildEventHandler(eventSource_BuildStartedHandler); eventSource.BuildFinishedEvent += new BuildEventHandler(eventSource_BuildFinishedHandler); if (Verbosity != LoggerVerbosity.Minimal) { eventSource.CustomEvent += new BuildEventHandler(eventSource_CustomHandler); eventSource.CommentEvent += new BuildEventHandler(eventSource_MessageHandler); eventSource.ProjectStartedEvent += new BuildEventHandler(eventSource_ProjectStartedHandler); eventSource.ProjectFinishedEvent += new BuildEventHandler(eventSource_ProjectFinishedHandler); if (Verbosity != LoggerVerbosity.Normal) { eventSource.TargetStartedEvent += new BuildEventHandler(eventSource_TargetStartedHandler); eventSource.TargetFinishedEvent += new BuildEventHandler(eventSource_TargetFinishedHandler); eventSource.TaskStartedEvent += new BuildEventHandler(eventSource_TaskStartedHandler); eventSource.TaskFinishedEvent += new BuildEventHandler(eventSource_TaskFinishedHandler); } } } } public override void Shutdown() { if (String.IsNullOrEmpty(this.outputPath)) { // Output to console. Console.WriteLine(this.outputDoc.OuterXml); } else { this.outputDoc.Save(this.outputPath); } } #region Event Handlers private void eventSource_BuildStartedHandler(object sender, BuildEventArgs e) { LogStageStarted(XmlLoggerElements.Build, e); } private void eventSource_BuildFinishedHandler(object sender, BuildEventArgs e) { LogStageFinished(e); } private void eventSource_ProjectStartedHandler(object sender, BuildEventArgs e) { LogStageStarted(XmlLoggerElements.Project, e); } private void eventSource_ProjectFinishedHandler(object sender, BuildEventArgs e) { LogStageFinished(e); } private void eventSource_TargetStartedHandler(object sender, BuildEventArgs e) { LogStageStarted(XmlLoggerElements.Target,e); } private void eventSource_TargetFinishedHandler(object sender, BuildEventArgs e) { LogStageFinished(e); } private void eventSource_TaskStartedHandler(object sender, BuildEventArgs e) { LogStageStarted(XmlLoggerElements.Task,e); } private void eventSource_TaskFinishedHandler(object sender, BuildEventArgs e) { LogStageFinished(e); } private void eventSource_MessageHandler(object sender, BuildEventArgs buildEvent) { string messageType; switch (buildEvent.Category) { case BuildEventCategory.Comment: if (buildEvent.Importance == BuildEventImportance.Low && Verbosity != LoggerVerbosity.Diagnostic) return; if (buildEvent.Importance == BuildEventImportance.Normal && (Verbosity != LoggerVerbosity.Diagnostic || Verbosity != LoggerVerbosity.Detailed || Verbosity != LoggerVerbosity.Normal)) return; messageType = XmlLoggerElements.Comment; break; case BuildEventCategory.Error: messageType = XmlLoggerElements.Error; break; case BuildEventCategory.Warning: messageType = XmlLoggerElements.Warning; break; // Shouldn't ever get here, but just in case default: messageType = "Unknown"; break; } LogMessage(messageType, sender, buildEvent); } private void eventSource_CustomHandler(object sender, BuildEventArgs e) { LogMessage(XmlLoggerElements.Custom, sender, e); } #endregion #region Logging private void LogStageStarted(string elementName, BuildEventArgs e) { // use the default root for the build element if (elementName != XmlLoggerElements.Build) { XmlElement stageElement = this.outputDoc.CreateElement(elementName); this.currentElement.AppendChild(stageElement); this.currentElement = stageElement; } if (verbose) SetAttribute(currentElement, e.TimeStamp, XmlLoggerAttributes.StartTime); } private void LogStageFinished(BuildEventArgs buildEvent) { if (buildEvent is BuildStatusEventArgs) { BuildStatusEventArgs buildStatusEvent = (BuildStatusEventArgs)buildEvent; SetAttribute(currentElement, buildStatusEvent.StageName, XmlLoggerAttributes.Name); SetAttribute(currentElement, buildStatusEvent.IsStageSuccessfullyFinished, XmlLoggerAttributes.Success); } if (verbose) SetAttribute(currentElement, buildEvent.TimeStamp, XmlLoggerAttributes.EndTime); if (this.currentElement.ParentNode is XmlElement) this.currentElement = (XmlElement)this.currentElement.ParentNode; } private void LogMessage(string messageType, object sender, BuildEventArgs buildEvent) { XmlElement messageElement = this.outputDoc.CreateElement(messageType); SetAttribute(messageElement, buildEvent.Importance, XmlLoggerAttributes.Importance); SetAttribute(messageElement, buildEvent.Code, XmlLoggerAttributes.Code); if (verbose || buildEvent.Category != BuildEventCategory.Comment) { SetAttribute(messageElement, buildEvent.File, XmlLoggerAttributes.File); SetAttribute(messageElement, buildEvent.LineNumber, XmlLoggerAttributes.LineNumber); SetAttribute(messageElement, buildEvent.ColumnNumber, XmlLoggerAttributes.ColumnNumber); } SetAttribute(messageElement, buildEvent.Processor, XmlLoggerAttributes.Processor); SetAttribute(messageElement, buildEvent.HelpKeyword, XmlLoggerAttributes.HelpKeyword); SetAttribute(messageElement, buildEvent.SubCategory, XmlLoggerAttributes.SubCategory); if (verbose) SetAttribute(messageElement, buildEvent.TimeStamp, XmlLoggerAttributes.TimeStamp); // Fill in the Message text if (!string.IsNullOrEmpty(buildEvent.Message)) { string temp = buildEvent.Message.Replace("&", "&"); // Escape < and > if this is not a "Properties" message. This is because in a Properties // message, we want the ability to embed legal XML, but otherwise we can get malformed // XML that will cause the parser to throw. if (buildEvent.Code != "Properties") { temp = temp.Replace("<", "<"); temp = temp.Replace(">", ">"); } messageElement.AppendChild(this.outputDoc.CreateCDataSection(temp)); } this.currentElement.AppendChild(messageElement); } private void SetAttribute(XmlElement element, object obj, string name) { if (obj == null) return; Type t = obj.GetType(); if (t == typeof(int)) { if (Int32.Parse(obj.ToString()) > 0) //???? { element.SetAttribute(name, obj.ToString()); } } else if (t == typeof(DateTime)) { DateTime dateTime = (DateTime)obj; element.SetAttribute(name, dateTime.ToString("G", DateTimeFormatInfo.InvariantInfo)); } else if (t == typeof(Boolean)) { element.SetAttribute(name, obj.ToString().ToLower()); } else if (t == typeof(BuildEventImportance)) { BuildEventImportance importance = (BuildEventImportance)obj; string attributeValue = ""; switch (importance) { case BuildEventImportance.Normal: attributeValue = XmlLoggerAttributeValues.Normal; break; case BuildEventImportance.Low: attributeValue = XmlLoggerAttributeValues.Low; break; case BuildEventImportance.High: attributeValue = XmlLoggerAttributeValues.High; break; // We should never have an importance that is not "Low", "High", // or "Normal", but just in case... default: attributeValue = "Unknown"; break; } element.SetAttribute(name, attributeValue); } else { if (obj.ToString().Length > 0) { element.SetAttribute(name, obj.ToString()); } } } #endregion #region Constants internal sealed class XmlLoggerElements { private XmlLoggerElements() { } public const string Build = "buildresults"; public const string Error = "error"; public const string Warning = "warning"; public const string Comment = "comment"; public const string Project = "project"; public const string Target = "target"; public const string Task = "task"; public const string Custom = "custom"; } internal sealed class XmlLoggerAttributes { private XmlLoggerAttributes() { } public const string Name = "name"; public const string File = "file"; public const string StartTime = "startTime"; public const string EndTime = "endTime"; public const string TimeStamp = "timeStamp"; public const string Code = "code"; public const string LineNumber = "line"; public const string ColumnNumber = "column"; public const string Importance = "level"; public const string Processor = "processor"; public const string HelpKeyword = "help"; public const string SubCategory = "category"; public const string Success = "success"; } internal sealed class XmlLoggerAttributeValues { private XmlLoggerAttributeValues() { } public const string Low = "low"; public const string Normal = "normal"; public const string High = "high"; } #endregion } } ------- end of XmlLogger.cs --------- ------- start of compile-msbuild.xsl --------- <?xml version="1.0"?> <!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp " "> ]> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:output method="html"/> <xsl:template match="/"> <xsl:variable name="errors" select="/cruisecontrol//buildresults//error" /> <xsl:variable name="errors.count" select="count($errors)" /> <xsl:variable name="warnings" select="/cruisecontrol//buildresults/warning" /> <xsl:variable name="warnings.count" select="count($warnings)" /> <xsl:if test="$errors.count > 0"> <table class="section-table" cellpadding="2" cellspacing="0" border="0" width="98%"> <tr> <td class="compile-sectionheader"> Errors (<xsl:value-of select="$errors.count"/>) </td> </tr> <tr> <td class="compile-error-data"> <xsl:apply-templates select="$errors"/> </td> </tr> </table> </xsl:if> <xsl:if test="$warnings.count > 0"> <table class="section-table" cellpadding="2" cellspacing="0" border="0" width="98%"> <tr> <td class="compile-sectionheader"> Warnings (<xsl:value-of select="$warnings.count"/>) </td> </tr> <tr> <td class="compile-warn-data"> <xsl:apply-templates select="$warning.events"/> </td> </tr> </table> </xsl:if> </xsl:template> <xsl:template match="error"> <div> <xsl:if test="@file != ''" > <xsl:value-of select="@file"/> (<xsl:value-of select="@line"/>,<xsl:value-of select="@column"/>): </xsl:if> Error <xsl:value-of select="@code"/>: <xsl:value-of select="text()" /> </div> </xsl:template> <xsl:template match="warning"> <div> <xsl:if test="@file != ''" > <xsl:value-of select="@file"/> (<xsl:value-of select="@line"/>,<xsl:value-of select="@column"/>): </xsl:if> Warning <xsl:value-of select="@code"/>: <xsl:value-of select="text()" /> </div> </xsl:template> </xsl:stylesheet> ------- end of compile-msbuild.xsl --------- <a href=http://mature-pic-post7130.blogspot.com/>post mature pic</a>
I have posted an updated version of the XmlLogger in my Blog (http://www.geekswithblogs.net/kobush/archive/2005/05/06/39153.aspx).
Need to decide on what we are doing about the MSBuild XmlLogger (e.g. whether we are providing a CCNet packaged logger) and update documentation and defaults accordingly.
XMLLogger download added to Confluence, source in CCNetContrib. Documentation updated including a tutorial. Good enough for 1.0, hopefully. :)
| |||||||||||||||||||||||||||||||||||||||||||||||
It might be necessary to add a <supportedruntime/> tag with the above versionnumber to ccnet.exe.config (not sure about that).
------- start of MSBuilder.cs ---------
using Exortech.NetReflector;
using System;
using System.Threading;
using System.IO;
using ThoughtWorks.CruiseControl.Core.Config;
using ThoughtWorks.CruiseControl.Core.Util;
using ThoughtWorks.CruiseControl.Remote;
using System.Text;
namespace ThoughtWorks.CruiseControl.Core.Builder
{
[ReflectorType("msbuild")]
public class MSBuilder : IBuilder
{
public const int DEFAULT_BUILD_TIMEOUT = 600;
public const string DEFAULT_EXECUTABLE = "msbuild.exe";
public const string DEFAULT_BASEDIRECTORY = ".";
public const string DEFAULT_LABEL = "NO-LABEL";
private ProcessExecutor _executor;
public MSBuilder() : this(new ProcessExecutor()) { }
public MSBuilder(ProcessExecutor executor)
{
_executor = executor;
}
[ReflectorProperty("executable", Required = false)]
public string Executable = DEFAULT_EXECUTABLE;
[ReflectorProperty("baseDirectory", Required = false)]
public string BaseDirectory = DEFAULT_BASEDIRECTORY;
[ReflectorProperty("buildFile", Required = false)]
public string BuildFile;
[ReflectorProperty("buildArgs", Required = false)]
public string BuildArgs;
[ReflectorArray("loggerList", Required = false)]
public string[] Loggers = new string[0];
[ReflectorArray("targetList", Required = false)]
public string[] Targets = new string[0];
/// <summary>
/// Gets and sets the maximum number of seconds that the build may take. If the build process takes longer than
/// this period, it will be killed. Specify this value as zero to disable process timeouts.
/// </summary>
[ReflectorProperty("buildTimeoutSeconds", Required = false)]
public int BuildTimeoutSeconds = DEFAULT_BUILD_TIMEOUT;
/// <summary>
/// Runs the integration using MSBuild. The build number is provided for labelling, build
/// timeouts are enforced. The specified targets are used for the specified MSBuild build file.
/// StdOut from msbuild.exe is redirected and stored.
/// </summary>
/// <param name="result">For storing build output.</param>
public void Run(IntegrationResult result)
{
ProcessResult processResult = AttemptExecute(CreateProcessInfo(result));
result.Output = processResult.StandardOutput;
if (processResult.TimedOut)
{
throw new BuilderException(this, "MSBuild process timed out (after " + BuildTimeoutSeconds + " seconds)");
}
if (processResult.ExitCode == 0)
{
result.Status = IntegrationStatus.Success;
}
else
{
result.Status = IntegrationStatus.Failure;
Log.Info("MSBuild build failed: " + processResult.StandardError);
}
}
private ProcessInfo CreateProcessInfo(IntegrationResult result)
{
ProcessInfo info = new ProcessInfo(Executable, CreateArgs(result), BaseDirectory);
info.TimeOut = BuildTimeoutSeconds*1000;
return info;
}
protected ProcessResult AttemptExecute(ProcessInfo info)
{
try
{
return _executor.Execute(info);
}
catch (Exception e)
{
throw new BuilderException(this, string.Format("Unable to execute: {0}\n{1}", BuildCommand, e), e);
}
}
private string BuildCommand
{
get { return string.Format("{0} {1}", Executable, BuildArgs); }
}
/// <summary>
/// Creates the command line arguments to the nant.exe executable. These arguments
/// specify the build-file name, the targets to build to,
/// </summary>
/// <returns></returns>
internal string CreateArgs(IntegrationResult result)
{
return string.Format("{0} {1} {2} {3} {4}", CreateBuildFileArg(), CreateLoggersArg(), BuildArgs, CreateLabelToApplyArg(result), CreateTargetsArg()).Trim();
}
private string CreateBuildFileArg()
{
if (StringUtil.IsBlank(BuildFile)) return string.Empty;
//return "-buildfile:" + BuildFile;
return BuildFile;
}
private string CreateLoggersArg()
{
if (Loggers.Length == 0) return string.Empty;
return string.Join(" /logger:", Loggers);
}
private string CreateTargetsArg()
{
if (Targets.Length == 0) return string.Empty;
return "/target:" + string.Join(";", Targets);
}
private string CreateLabelToApplyArg(IntegrationResult result)
{
string label = StringUtil.IsBlank(result.Label) ? DEFAULT_LABEL : result.Label;
return "/property:label-to-apply=" + label;
}
public bool ShouldRun(IntegrationResult result)
{
return result.Working && result.HasModifications();
}
public override string ToString()
{
return string.Format(@" BaseDirectory: {0}, Targets: {1}, Executable: {2}, BuildFile: {3}", BaseDirectory, CreateTargetsArg(), Executable, BuildFile);
}
}
}
----- end of MSBuilder.cs ------
the build node in the config file looks like this:
-----
<build type="msbuild">
<executable>C:\WINDOWS\Microsoft.NET\Framework\v2.0.40607\MSBuild.exe</executable>
<baseDirectory>d:\temp\MyProject</baseDirectory>
<buildArgs>@buildfile.response</buildArgs>
<buildFile>MyProject.sln</buildFile>
<targetList>
<target>All</target>
</targetList>
<!-- loggerList is untested, as I don't have any MSBuild loggers yet,
see the 3 msbuild articles on MSDN for more info on loggers -->
<loggerList>
<logger>SolutionLogger,ACMELogger;file="Build.txt";verbose=true</logger>
</loggerList>
<buildTimeoutSeconds>300</buildTimeoutSeconds>
</build>
-----
All that remains to be done is to create a NAnt-like xml logger for msbuild so that build output can be merged with ccnet build logs.