Running StyleCop from the command line and in a TFS 2015 vNext build

Updated 6 Feb 2016 - See the newer post about the new vNext build task I have written to do the same job

Virtually any automated build will require some customisation beyond a basic compile. So as part of my upcoming Techorama session on TFS 2015 vNext build I need a demo of using a custom script as part of the build process. Two common customisations we use are version stamping of assemblies and running code analysis tools. For vNext build there is already a sample of version stamping, so I thought getting StyleCop running would be a good sample.

The problem

Customisation in vNext build is based around running a script, in the case of a Windows based build agents this a PowerShell script. The problem with StyleCop is that it does not provide a command line iterface. The  StyleCop CodePlex project provides only a Visual Studio add-in. There is also the ALM Ranger’s TFS community custom build  activity, but I could find no current command line interface projects.

So I needed to build one.

Step 1 – Create a command line

So my first step was to create a command line version of StyleCop. I chose to use the community build activity as a starting point. I had planned to do this all in PowerShell, but quickly found that the conversion of parameter object types and the handling of the events StyleCop uses was a bit messy. So I decided to write a wrapper class in C# that presented the same parameters as the old TFS build activity, basically take the old code and remove the Windows Workflow logic. I then provided a Main (args) method to expose the object to the command line such that it was easy to provide the required parameters.

This can all be found on my GitHub site.

Note on solution structure: As I wanted this to work for PowerShell and the command prompt I had to place the Main(args[]) method .EXE entry point in a project that built an EXE and all the rest of the wrapper code in one that built a .DLL. This is because you cannot load a type in PowerShell using add-type from an assembly built as an EXE, you get a EXTENSION_NOT_SUPPORTED exception. It means there are two projects (a DLL and an EXE) when I would really have like a single one (the EXE)

So I now had a command line I could call from my PowerShell script

1StyleCopCmdLine --f="File1.cs" "File2.cs" --s="AllSettingsEnabled.StyleCop"

A good starting point. However,  more a TFS build it makes more sense to call StyleCop directly in the PowerShell, why shell out to a command prompt to run an EXE when your can run the code directly in PowerShell?

Step 2 – Create a simple PowerShell script

The PowerShell required to run StyleCop using the wrapper is simple, just providing the same parameters as used for the EXE.

1Add-Type -Path "StyleCopWrapper.dll"

$scanner = new-object StyleCopWrapper.Wrapper
$scanner.MaximumViolationCount = 1000
$scanner.ShowOutput = $true
$scanner.CacheResults = $false
$scanner.ForceFullAnalysis = $true
$scanner.XmlOutputFile = "$pwdout.xml"
$scanner.LogFile = "$pwdlog.txt"
$scanner.SourceFiles =  @("file1.cs", "file2.cs") )
$scanner.SettingsFile = "settings.stylecop"
$scanner.AdditionalAddInPaths = @("C:Program Files (x86)StyleCop 4.7" )
$scanner.TreatViolationsErrorsAsWarnings = $false

$scanner.Scan()

write-host ("Succeeded [{0}]" -f $scanner.Succeeded) write-host ("Violation count [{0}]" -f $scanner.ViolationCount)

See the GitHub site’s WIKI for the usage details.

Step 3 – Create a vNext build PowerShell script

So now we have the basic tools we need to run StyleCop from a TFS vNext build, but we do need a more complex script.

The script you use is up to you, mine looks for .csproj files and runs StyleCop recursively from the directories containing the .csproj files. This means I can have a different  setting.stylecop file for each project. In general I have more strict rules on production code than unit test e.g. for unit tests I am not bother about the XML method documentation, but for production code I make sure they are present and match the method parameters.

Note: As the script just uses parameters and environment variable it is easy to test outside TFS build, a great improvement over the old build system

 1#  
 2# Script to allow StyleCop to be run as part of the TFS vNext build  
 3#  
 4[CmdletBinding()]  
 5param  
 6(  
 7    # We have to pass this boolean flag as string, we cast it before we use it  
 8    # have to use 0 or 1, true or false  
 9    [string]$TreatStyleCopViolationsErrorsAsWarnings = 'False'  
10)

# local test values, should be commented out in production
#$Env:BUILD_STAGINGDIRECTORY = "C:drops"
#$Env:BUILD_SOURCESDIRECTORY = "C:codeMySolution"

if(-not ($Env:BUILD_SOURCESDIRECTORY -and $Env:BUILD_STAGINGDIRECTORY))
{
    Write-Error "You must set the following environment variables"
    Write-Error "to test this script interactively."
    Write-Host '$Env:BUILD_SOURCESDIRECTORY - For example, enter something like:'
    Write-Host '$Env:BUILD_SOURCESDIRECTORY = "C:codeMySolution"'
    Write-Host '$Env:BUILD_STAGINGDIRECTORY - For example, enter something like:'
    Write-Host '$Env:BUILD_STAGINGDIRECTORY = "C:drops"'
    exit 1
}

# pickup the build locations from the environment
$stagingfolder = $Env:BUILD_STAGINGDIRECTORY
$sourcefolder = $Env:BUILD_SOURCESDIRECTORY

# have to convert the string flag to a boolean
$treatViolationsErrorsAsWarnings = [System.Convert]::ToBoolean($TreatStyleCopViolationsErrorsAsWarnings)

Write-Host ("Source folder (`$Env)  [{0}]" -f $sourcefolder) -ForegroundColor Green
Write-Host ("Staging folder (`$Env) [{0}]" -f $stagingfolder) -ForegroundColor Green
Write-Host ("Treat violations as warnings (Param) [{0}]" -f $treatViolationsErrorsAsWarnings) -ForegroundColor Green
 
# the overall results across all sub scans
$overallSuccess = $true
$projectsScanned = 0
$totalViolations = 0

# load the StyleCop classes, this assumes that the StyleCop.DLL, StyleCop.Csharp.DLL, # StyleCop.Csharp.rules.DLL in the same folder as the StyleCopWrapper.dll $folder = Split-Path -parent $MyInvocation.MyCommand.Definition Write-Host ("Loading from folder from [{0}]" -f $folder) -ForegroundColor Green $dllPath = [System.IO.Path]::Combine($folder,"StyleCopWrapper.dll") Write-Host ("Loading DDLs from [{0}]" -f $dllPath) -ForegroundColor Green Add-Type -Path $dllPath

$scanner = new-object StyleCopWrapper.Wrapper

# Set the common scan options,
$scanner.MaximumViolationCount = 1000
$scanner.ShowOutput = $true
$scanner.CacheResults = $false
$scanner.ForceFullAnalysis = $true
$scanner.AdditionalAddInPaths = @($pwd) # in in local path as we place stylecop.csharp.rules.dll here
$scanner.TreatViolationsErrorsAsWarnings = $treatViolationsErrorsAsWarnings

# look for .csproj files
foreach ($projfile in Get-ChildItem $sourcefolder -Filter *.csproj -Recurse)
{
   write-host ("Processing the folder [{0}]" -f $projfile.Directory)

   # find a set of rules closest to the .csproj file
   $settings = Join-Path -path $projfile.Directory -childpath "settings.stylecop"
   if (Test-Path $settings)
   {
        write-host "Using found settings.stylecop file same folder as .csproj file"
        $scanner.SettingsFile = $settings
   }  else
   {
       $settings = Join-Path -path $sourcefolder -childpath "settings.stylecop"
       if (Test-Path $settings)
       {
            write-host "Using settings.stylecop file in solution folder"
            $scanner.SettingsFile = $settings
       } else
       {
            write-host "Cannot find a local settings.stylecop file, using default rules"
            $scanner.SettingsFile = "." # we have to pass something as this is a required param
       }
   }

   $scanner.SourceFiles =  @($projfile.Directory)
   $scanner.XmlOutputFile = (join-path $stagingfolder $projfile.BaseName) +".stylecop.xml"
   $scanner.LogFile =  (join-path $stagingfolder $projfile.BaseName) +".stylecop.log"
   
   # Do the scan
   $scanner.Scan()

    # Display the results
    Write-Host ("`n")
    write-host ("Base folder`t[{0}]" -f $projfile.Directory) -ForegroundColor Green
    write-host ("Settings `t[{0}]" -f $scanner.SettingsFile) -ForegroundColor Green
    write-host ("Succeeded `t[{0}]" -f $scanner.Succeeded) -ForegroundColor Green
    write-host ("Violations `t[{0}]" -f $scanner.ViolationCount) -ForegroundColor Green
    Write-Host ("Log file `t[{0}]" -f $scanner.LogFile) -ForegroundColor Green
    Write-Host ("XML results`t[{0}]" -f $scanner.XmlOutputFile) -ForegroundColor Green

    $totalViolations += $scanner.ViolationCount
    $projectsScanned ++
   
    if ($scanner.Succeeded -eq $false)
    {
      # any failure fails the whole run
      $overallSuccess = $false
    }

}

# the output summary Write-Host ("`n") if ($overallSuccess -eq $false) {    Write-Error ("StyleCop found [{0}] violations across [{1}] projects" -f $totalViolations, $projectsScanned) } elseif ($totalViolations -gt 0 -and $treatViolationsErrorsAsWarnings -eq $true) {     Write-Warning ("StyleCop found [{0}] violations warnings across [{1}] projects" -f $totalViolations, $projectsScanned) } else {    Write-Host ("StyleCop found [{0}] violations warnings across [{1}] projects" -f $totalViolations, $projectsScanned) -ForegroundColor Green }

Step 4 – Adding a the script to the repo

To use the script it needs (and any associated files) to be placed in your source control. In my case it meant I create a folder called StyleCop off the root of my TFS 2015 CTP’s Git repo and in it placed the following files

  • PowerShell.ps1 – my script file
  • StyleCop.dll – the main StyleCop assembly taken from c:program files (x86)StyleCop 4.7. By placing it here it means we don’t need to actually install StyleCop on the build machine
  • StyleCop.csharp.dll – also from c:program files (x86)StyleCop 4.7
  • StyleCop.csharp.rules.dll – also from c:program files (x86)StyleCop 4.7
  • StyleCopWrapper.dll – the wrapper assembly from my GitHub site

Step 5 – Adding the script to a build process

Once the script is in the repo adding a new step to a vNext build is easy.

  • In a browser select the Build.vNext menu options

  • The build explorer will be show, right click on the build you wish to add a step to and select edit

  • Press the ‘Add build step’ button. The list of steps will be show, pick PowerShell

    image

  • As the script is in the repo we can reference it in the new step. in my case I set the script file name to

    _                      StyleCop/PowerShell.ps1
    _

  • My script takes one parameter, if we should treat StleCop violations as warnings, this is set as the script argument. Note I am using a build variable $(ViolationsAsWarnings) set to a string value ‘True’ or ‘False’, so I have one setting for the whole build script. Though a boolean parameter would be nice it seems I can only pass in strings as build variables, so I do the conversion to a boolean inside the script.

                          _-TreatStyleCopViolationsErrorsAsWarnings $(ViolationsAsWarnings)

    image _

Step 6 - Running the build

My test solution has two projects, with different settings.stylecop files. Once the new step was added to my build I could queue a build, by altering $(ViolationsAsWarnings)  variable I could make the build pass for fail.

image

       image

The detailed StyleCop result are available in the build log and are also placed in the drops folder in an XML format.

Note: One strange behaviour is that when you test the script outside TFS build you get a .XML and .LOG file for each project scanned. In TFS build you only see the .XML file in the drops folder, this I think is because the .LOG has been redirected into the main TFS vNext build logs.

Summary

So now I have a way to run StyleCop within a TFS vNext build.

Using these techniques there are no end of tools that can be wired into the build process, and I must say it is far easier than the TFS 2010, 2012, 2013 style workflow customisation.