MsBuild Version Gotchas – Better Building with MsBuild scripts and Build Servers

I recently ran into issues when upgrading an old codebase from the original setup (Visual Studio 2013 and the MsBuild tools v4.0) to use Visual Studio 2015, building through Team City on a new machine that only had VS2015. The MsBuild scripts I had been using for building (mostly creating the deployment package) and actual deployment (calling MsDeploy) needed a few tweaks. Mostly, I had taken and adapted a boilerplate MsBuild script from an earlier project, left in some stupid hacks that worked well until they didn’t, and found my scripts needed a few tweaks to work with the current tools.

My build process was to use Team City to compile, package, and later deploy (via MsDeploy) a web app to Azure, using MsBuild scripts to manage the actual solution build/package/deploy specifics.

For the purposes of this post, Team City happens to be my build server of choice. If the detail of your build process is stored and versioned with the code (and not the build server configs) then switching build servers should be easy.

So this post is some note-to-future-self gotchas for the next time a tools upgrade breaks the build…

MsBuild.exe path and version

First issue was to check the location of the MsBuild.exe itself. As this wasn’t already in my path, I had been specifying it explicitly as a command prompt setup for locally debugging build scripts, using a “commandPrompt.bat” with the following line

@%comspec% /k "SET PATH=C:\Windows\Microsoft.NET\Framework\v4.0.30319;

So first issue is to fix the path and use the latest version – “C:\Program Files (x86)\MSBuild\14.0\Bin”.

Setting the local path to MsBuild isn’t strictly required – normally I’d build locally through Visual Studio, but I like being able to test the build scripts locally without having to push them to the build server, and wrapping the build specifics in an MsBuild script lets me do this. The important thing is that I want Visual Studio and Team City build to compile the code with the same MsBuild engine so there’s no surprises.

Whenever I run the MsBuild script eg. with a

msbuild WebPackage.msbuild /t:BuildPackage

I get the MsBuild engine version reported, but to help debug my scripts, I tend to put some kind of “Info” target in to report paths and settings, eg:

<Target Name="Info">
 <Message Text="MsBuildExtensionsPath=$(MsBuildExtensionsPath)" />
 <Message Text="MSBuildToolsVersion=$(MSBuildToolsVersion)"/>
 <Message Text="VisualStudioVersion=$(VisualStudioVersion)"/>
 <Message Text="VSToolsPath=$(VSToolsPath)"/>

Now I can run:

msbuild WebPackage.msbuild /t:Info

If the latest MsBuild is on the path, I should get latest version reported (v14.0).

Also, in my .csproj file I found references in a PropertyGroup to “VisualStudioVersion” and “VSToolsPath” settings.

 <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
 <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v(VisualStudioVersion)</VSToolsPath>

Your build scripts shouldn’t really be tying themselves to specific tools versions, better to set these values and pass them in to any running MsBuild process.

MsBuild Scripts Calling MsBuild

Noticed another problem – I had my MsBuild script calling the MsBuild.exe directly:

<Exec Command="$(MSBuildExe) $(SolutionName).sln /t:clean"/>

…and because I couldn’t guarantee MsBuild.exe in the path, I cheated and had my MsBuild script setting the MsBuild.exe path to a default if it wasn’t overridden:

<MSBuildExe Condition=" '$(MSBuildExe)'=='' ">C:\Windows\Microsoft.NET\Framework\v4.0.30319\msbuild.exe</MSBuildExe>

So checking the output, I was using the old MsBuild engine version. Oops. I could probably have allowed the .exe location to be specified either on the path, as an argument to the script, or as a default in the script, but that’s a hassle. Luckily, to free myself from nasty tools path dependencies, I can rewrite my MsBuild calls as:

<MSBuild Projects="$(SolutionName).sln" Targets="clean"/>

The general format is:

<MSBuild Projects="filename" Targets="target" Properties="prop1=val1;prop2=val2"/>

This should mean that whatever MsBuild engine you use to kick off the script is the one that gets used throughout. I have had issues with a few quotes around property values (solution: don’t quote property values), but it should be possible to express all MsBuild calls using the MsBuild task.

Importing MsBuild Tasks

Ok, for some reason, probably relating to some hacky web.config transforming I had done on previous projects (I have done terrible things to get applications to deploy and workaround the “configure-build-deploy” workflow of web.configs), I had the following using statement in my MsBuild script:

<UsingTask TaskName="TransformXml" AssemblyFile="C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll"/>

This was actually referencing non-existent DLL

C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll

Which should be

C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0\Web\Microsoft.Web.Publishing.Tasks.dll

There’s another issue here – this will almost certainly require Visual Studio installed on the build server, or a cut down install eg.

Turns out, I actually no longer needed to use this task for building this project and I can just omit this line. But there will be times when your build script needs access to those extra tasks. If you can, try to avoid your build script knowing exactly where on the build server to look for stuff.

Team City Settings

So finally, I had to go into my Team City build configuration for all my build steps that used MsBuild and upgrade the MsBuild tools versions to latest. And check you’re using the same version throughout the build (I ran into problems compiling and packaging the same web app with different MsBuild versions in different Team City build steps).


Things to watch for:

  • Wrong MsBuild version on path – to debug, create an “Info” target in your build script and echo property values to console / build server log
  • Specifying the MsBuild version in your script – use the “MsBuild” task instead of direct MsBuild.exe calls to prevent specifying the .exe location on the path or in your script
  • References to a specific Visual Studio / tools version that may not be installed – eg. common MsBuild targets
  • Copying-and-pasting a previous build script and not cleaning out the unused stuff when you find a better way of doing things

Also – limit the use of MsBuild, try keep it simple, use it for building, packaging, including/excluding/copying files to be deployed. Handle deployment and other complex tasks through Powershell.