Monday, 23 May 2011

Turn on "Warnings as Errors" for all projects in a solution

It's a habit of mine to always try to address any compiler warnings, as more often than not a warning is an indication that something is wrong with your code (even though it is still may be compilable). One way to ensure this is to make warnings cause a build to fail (just like errors do).

For a single project, you can turn this on within the Build tab of the Project Properties page:

Treat warnings as errors in Project Properties

If you have a large solution with lots of existing projects, this can be a bit tedious so I wrote a PowerShell script to automate the process. It finds all C# projects, and for each .csprof file updates every configuration block found and adds a new <TreatWarningsAsErrors> element. (If you don't use TFS then just comment out the line that calls TF.EXE)

 

Get-ChildItem -Recurse -Filter "*.*csproj" | % {
    Write-Host $_.Name
    
    $filename = $_.Fullname
    
    $proj = [xml]( Get-Content $_.Fullname )

    $xmlNameSpace = new-object System.Xml.XmlNamespaceManager($proj.NameTable)

    $xmlNameSpace.AddNamespace("p", "http://schemas.microsoft.com/developer/msbuild/2003")
    
    $nodes = $proj.SelectNodes("/p:Project/p:PropertyGroup[@Condition and not (p:TreatWarningsAsErrors)]", $xmlNameSpace)
    
    $touched = $false
        
    $nodes | ForEach-Object -Process { 
        $e = $proj.CreateElement("TreatWarningsAsErrors", "http://schemas.microsoft.com/developer/msbuild/2003")
        $e.set_InnerText("true")
        $_.AppendChild($e) | Out-Null
        $touched = $true
    }
    
    if ($touched) {
        Write-Host "Checkout $filename"
        & "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\TF.exe" checkout $filename | Out-Null
    
        $proj.Save("$($filename)") | Out-Null
    }
}

Wednesday, 18 May 2011

Checking build durations over time

I've written a PowerShell script to iteratively grab changesets from TFS, and do a build of the solution from that changeset. For each build I record the time taken. This is useful if you think builds are taking longer than they used to and you need some quantitative evidence. You can save the output to a CSV file and open it up in Excel to display a pretty graph etc.

To use the script, you'll need to have installed the Team Foundation Server Power Tools which include PowerShell cmdlets for TFS.

# Iteratively build changesets from TFS to measure build speed
#
# Run this from a folder that is mapped as the TFS workspace

# Load VSVARS.BAT (From http://blogs.msdn.com/b/ploeh/archive/2008/04/09/visualstudio2008powershell.aspx)
function VsVars32()
{
    $vs100comntools = (Get-ChildItem env:VS100COMNTOOLS).Value
    $batchFile = [System.IO.Path]::Combine($vs100comntools, "vsvars32.bat")
    Get-Batchfile $BatchFile
}

function Get-Batchfile ($file) {
    $cmd = "`"$file`" & set"
    cmd /c $cmd | Foreach-Object {
        $p, $v = $_.split('=')
        Set-Item -path env:$p -value $v
    }
}

VsVars32

# Get changeset history since 1-Jan-2011 and checked in by me
$history = Get-TfsItemHistory "$/Project/Branch" -Version "D01/01/11~" -Recurse | Where-Object { $_.Owner -eq "DOMAIN\username" }

# Process changesets from earliest to latest
[array]::Reverse($history)

$tf = "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\TF.exe"

$history | % { 
    & $tf get /version:C$($_.ChangesetId) /Recursive /Overwrite /NoPrompt 2>&1 | Out-Null

    # clean first
    & "msbuild.exe" /t:Clean /noconlog /v:q /nologo 2>&1 | Out-Null
    
    # build
    $duration = Measure-Command { & "msbuild.exe" /noconlog /v:q /nologo  }

    if ($LastExitCode -eq 0)
    {
        Write-Output "$($_.ChangesetId), $($duration.TotalSeconds)"
    }
    else {
        Write-Warning "$($_.ChangesetId) build failed"
    }    
}

Monday, 9 May 2011

Visual Studio network performance with TFS through a VPN

I've been accessing a TFS server remotely through a VPN connection. I'd noticed that the performance was not that great, and then discovered this suggestion to specify the defaultProxy element in the devenv.exe.config file.

That worked pretty well, but I noticed that things were still a bit sluggish, so added a bypasslist element too, to ensure that anything on a 192.168.0.0 network doesn't use the proxy either. That's working nicely now.

<?xml version="1.0" encoding="utf-8"?>
<system.net>
   <settings>
     <ipv6 enabled="true"/>
   </settings>
   <defaultProxy enabled="true" useDefaultCredentials="true">
     <proxy bypassonlocal="True" proxyaddress=http://proxy-server:8888/>
     <bypasslist>
       <add address="192\.168\.\d{1,3}\.\d{1,3}" />
     </bypasslist>
   </defaultProxy> </system.net>

Wednesday, 4 May 2011

Updating multiple lines in Visual Studio project files

A while back I blogged about how to change text from a single line in a Visual Studio project file. What about when you need to replace a number of lines of text? This worked for me:

$regex = new-object Text.RegularExpressions.Regex "\<Target Name=`"AfterBuild`"\>.+\</Target\>", ('singleline')
Get-ChildItem -recurse -filter "*.*proj" | % { $text = (Get-Content $_.FullName) -join "`r`n"; $regex.Replace($text, "<Import Project=`"`$(SolutionDir)\Module.targets`" />`r`n") | Set-Content "$($_.FullName).tmp" }

We have to do a little more work compared to the first approach. We need to join all the individual lines into one single line so that the regex will operate correctly. Because we want to set options on the regex, we create that separately too. Note that the above example writes to a new .tmp file, so you can confirm the replace is working properly before overwriting the original files.

Project files are just XML files, so this technique should work for any XML file.

In my case, the solution was checked in to TFS, so the project files will need to be checked out before they can be updated. The checkout can be incorporated into the pipeline like this:

Get-ChildItem -recurse -filter "*.Service.*proj" | % { & "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\TF.exe" checkout $_.FullName; $text = (Get-Content $_.FullName) -join "`r`n"; $regex.Replace($text, "<Import Project=`"`$(SolutionDir)\Web.Module.targets`" />`r`n") | Set-Content "$($_.FullName).tmp" }

References: