Wednesday, October 11, 2006

 

Integrating FxCop into CruiseControl.NET

Recently, I needed to integrate FxCop into our CC.NET server.  I had a few design goals for the integration.
I found couple useful resources along the way:
I discovered fairly quickly that FxCop locks the assemblies that it's analyzing, which meant that I had to make a copy of the assemblies that FxCop was analyzing or else the build project would fail because it couldn't copy to its output directory.  I also discovered that the return value of FxCopCmd doesn't indicate whether rule violations were found, rather whether a catastrophic error was encountered.  That meant that to get the CC.NET build to break, I had to run FxCop from another application to control the value that was returned to CC.NET.  I decided to accommodate both of those needs with msbuild.  I could've used NAnt for the same purpose.  Specifically, to break the CC.NET build I needed msbuild to examine the FxCop output and return a failing error code if any violations were found.  The ability to parse an XML file isn't in the base functionality of msbuild; however,  the <XmlRead> task of MSBuild Community Tasks Project does have that ability.

The steps for my msbuild script are:

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

  <Target Name="RunCheck">
    <CallTarget Targets="Copy" />
    <CallTarget Targets="Check" />
    <CallTarget Targets="Report" />
  </Target>

  <Target Name="RunFxCopUI">
    <CallTarget Targets="Copy" />
    <Exec Command="attrib +R $(FxCopProject)" />
    <Exec Command='"$(FxCopExe)" $(FxCopProject)'/>
  </Target>

  <Target Name="Copy">
    <Copy SourceFiles="@(DllsAndPdbs)" DestinationFolder="$(FxCopWorkDirectory)" SkipUnchangedFiles="true"/>
  </Target>

  <Target Name="Check">
    <Exec Command='"$(FxCopCmdExe)" /project:$(FxCopProject) /out:$(FxCopOutput) /directory:$(FxCopWorkDirectory) /forceoutput' />
  </Target>

  <Target Name="Report">
    <XmlRead ContinueOnError="True" XmlFileName="$(FxCopOutput)" XPath="string(count(//Issue[@Level='CriticalError']))">
      <Output TaskParameter="Value" PropertyName="FxCopCriticalErrors" />
    </XmlRead>
    <XmlRead ContinueOnError="True" XmlFileName="$(FxCopOutput)" XPath="string(count(//Issue[@Level='Error']))">
      <Output TaskParameter="Value" PropertyName="FxCopErrors" />
    </XmlRead>
    <XmlRead ContinueOnError="True" XmlFileName="$(FxCopOutput)" XPath="string(count(//Issue[@Level='CriticalWarning']))">
      <Output TaskParameter="Value" PropertyName="FxCopCriticalWarnings" />
    </XmlRead>
    <XmlRead ContinueOnError="True" XmlFileName="$(FxCopOutput)" XPath="string(count(//Issue[@Level='Warning']))">
      <Output TaskParameter="Value" PropertyName="FxCopWarnings" />
    </XmlRead>

    <Math.Add Numbers="$(FxCopCriticalErrors);$(FxCopErrors);$(FxCopCriticalWarnings);$(FxCopWarnings)">
      <Output TaskParameter="Result" PropertyName="FxCopRuleViolations" />
    </Math.Add>

    <Error Text="FxCop encountered $(FxCopRuleViolations) rule violation(s). Critical errors: $(FxCopCriticalErrors). Errors: $(FxCopErrors). Critical warnings: $(FxCopCriticalWarnings). Warnings: $(FxCopWarnings)."
Condition="$(FxCopRuleViolations) &gt; 0" />
  </Target>

  <ItemGroup>
    <DllsAndPdbs Include="..\..\..\ThirdParty\lib\*.dll;bin\debug\*.dll;bin\debug\*.pdb;"/>
  </ItemGroup>

  <PropertyGroup>
    <FxCopWorkDirectory>FxCop</FxCopWorkDirectory>
    <FxCopProject>ProjectName.FxCop</FxCopProject>
    <FxCopOutput>$(FxCopWorkDirectory)\ProjectName.FxCop.output.xml</FxCopOutput>
  </PropertyGroup>

  <ItemGroup>
    <OutputFiles Include="FxCop\*.dll;FxCop\*.pdb" />
  </ItemGroup>

  <PropertyGroup>
    <FxCopCriticalErrors>0</FxCopCriticalErrors>
    <FxCopErrors>0</FxCopErrors>
    <FxCopCriticalWarnings>0</FxCopCriticalWarnings>
    <FxCopWarnings>0</FxCopWarnings>
  </PropertyGroup>

  <PropertyGroup>
    <ExpectedFxCopCmdPath>C:\Program Files\Microsoft FxCop 1.35\FxCopCmd.exe</ExpectedFxCopCmdPath>
  </PropertyGroup>

  <Choose>
    <When Condition="Exists($(ExpectedFxCopCmdPath))">
      <!-- Hope that the expected version of FxCop is installed -->
      <PropertyGroup>
        <FxCopCmdExe>$(ExpectedFxCopCmdPath)</FxCopCmdExe>
      </PropertyGroup>
    </When>
    <Otherwise>
      <!-- Otherwise hope that FxCop is in the path. -->
      <PropertyGroup>
        <FxCopCmdExe>fxcopcmd.exe</FxCopCmdExe>
      </PropertyGroup>
    </Otherwise>
  </Choose>
</Project>

To get the output of FxCop to appear on the CC.NET dashboard, I needed to tell FxCop to write its output to a file.  Use the "/out:<filename>" switch for that.  The "/forceoutput" switch is also nice because it causes an output file to be written even if no rules are violated.  Then I needed to merge that into the CC.NET build log.  The CC.NET merge task is intended for that.  The gotcha I discovered is that I needed to put the <merge> task under the <publishers> tag, not the <tasks> tag.  If it's under <tasks>, it's not executed if the build fails, which foils my goal of getting the rule violations onto the CC.NET dashboard.

That leaves only the question of how to cause the FxCop CC.NET project to run after the main project completes successfully.  CC.NET includes a <projectTrigger>, which is perfect for this task.  Not only is the project trigger able to run the dependent project only when the parent project completes successfully, but it also is smart enough not to run the dependent project multiple times if the parent project completes while the child is still running.  Here's my ccnet.config:

<cruisecontrol>
  <project name="FxCop">
    <publishExceptions>true</publishExceptions>
    <triggers>
      <projectTrigger project="ParentProject">
        <triggerStatus>Success</triggerStatus>
      </projectTrigger>
    </triggers>
    <tasks>
      <msbuild>
        <timeout>1800</timeout>
        <executable>C:\Windows\Microsoft.NET\Framework\v2.0.50727\msbuild.exe</executable>
        <projectFile>ProjectFile.Fxcop.msbuild.xml</projectFile>
        <buildArgs>/noconsolelogger /v:diag</buildArgs>
        <logger>ThoughtWorks.CruiseControl.MsBuild.XmlLogger,ThoughtWorks.CruiseControl.MsBuild.dll</logger>
      </msbuild>
    </tasks>
    <publishers>
      <merge>
        <files> <file>FxCop\ProjectName.FxCop.output.xml</file> </files>
      </merge>
      <xmllogger />
    </publishers>
  </project>
</cruisecontrol>
My biggest disappointment with the result is that I couldn't get the detailed FxCop results to appear on the CC.NET web dashboard page for each build; rather they appear on the "FxCop Report" page, which I decided was not worth the effort to change.  However, I really didn't like the look of the standard report, so I edited my CruiseControl\webdashboard\dashboard.config file to use Brian Likes's XSL.

Comments:
i keep getting this error i dont know why

MSB3073: The command "FxCopCmd.exe /project:C:\Projects\Main\Src\Sln\Phoenix.fxcop /out:C:\Public\BuildLogs\fxcop\Phoenix.FxCop.xml" exited with code 4. in Phoenix.msbuild(6, 5)

when i try running it from command line manually it works

any suggestions. help
 
Post a Comment

<< Home

This page is powered by Blogger. Isn't yours?