Building VS 2008 Unit Test Projects in MSBuild 4.0 Beta 2

Introduction

MSBuild 4.0 has all sorts of features for targeting different .NET Framework versions. The idea is that you can use MSBuild 4.0 to build all your legacy (pre-4.0) project types, as well as new projects that just target a downlevel version of the .NET Framework. In addition, you can mix and match the target framework versions for different projects under a single invocation of MSBuild.exe. Unfortunately, in Beta 2, we've had reports of a problem with building unit test projects that contain the special generated accessor assemblies, otherwise known as "test references". If you have a VS 2008 unit test project with test references, you may find that MSBuild 4.0 gives you an error like:

Caught a BadImageFormatException saying "Could not load file or assembly 'C:\Build\Project\Sources\ProjectName\ProjectName.UnitTests\obj\Debug\ProjectName_Accessor.dll' or one of its dependencies. This assembly is built by a runtime newer than the currently loaded runtime and cannot be loaded.".

Update: This should be fixed for the final release of VSTS 2010.

The Problem

MSBuild 4.0 uses the .NET 4.0 CLR. All the MSBuild tasks, including third party and custom tasks, are loaded under the 4.0 runtime. This, in most cases, isn’t bad, because the 4.0 runtime should be perfectly capable of running code that was targeted for the 2.0 runtime. However, in the case of the VS 2008 Team Test BuildShadowTask, the code creates an assembly using the System.Reflection.Emit namespace. That namespace, when provided by the 2.0 CLR emits assemblies targeting the 2.0 CLR. When the namespace is provided by the 4.0 CLR it emits assemblies targeted at the … drum roll please … 4.0 CLR! So, when you build a VS 2008 unit test project (targeting the 3.5 framework, which uses the 2.0 CLR) using MSBuild 4.0, you end up with accessor assemblies that target the 4.0 CLR. Oops. Now downlevel applications trying load the assembly will blow up with the above exception. You’ll see it sometimes during your build with ResGen.exe, or when you go to run your tests from MSTest.exe.

The Solution Workaround

To get around this, all you need to do is run the BuildShadowTask from MSBuild 3.5, which is the version VS 2008 projects were meant to be built with. However, if you are using build features in TFS 2010 Beta 2, you are in trouble, since it (out-of-the-box) only supports MSBuild 4.0. Sure, you could write your own build activity to run MSBuild 3.5, but MSBuild 4.0 is *supposed* to work. So rather than blaming the TFS Build team (of which I am a part of) I’ll address the real issue. Several teams have gained traction to resolve this bug before shipping, but there is no guarantee. Quite frankly, this bug is minor in regards to the entire project. My team is pushing heavily for it to get done, but time will only tell.

Just as an extra note, the problem is actually in the VS 2008 Microsoft.TeamTest.targets file, or the BuildShadowTask that it uses (depending on how you look at it). They generate an assembly targeting the wrong framework version. So that is a natural place to fix the issue. Instead of modifying the .targets file directly (you could, but think of all the things that could go wrong!) I decided to add a new .targets file, and then modify my unit test projects to load it instead of the original. The new .targets will replace the ResolveTestReferences target that Microsoft.TeamTest.targets exposes.

Fast Track

If you just want to get it done and don’t care to know how it works, do this:

  1. Download the attached file and place the .targets file in %ProgramFiles%\MSBuild\Microsoft\VisualStudio\9.0\TeamTest (or %ProgramFiles(x86)%\MSBuild\Microsoft\VisualStudio\9.0\TeamTest on an x64 system).

  2. Add the following property to your unit test project (.csproj or .vbproj) files near the beginning (it needs to be before the import of Microsoft.CSharp.targets).

    <MsTestToolsTargets>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v9.0\TeamTest\Microsoft.TeamTest.4.targets</MsTestToolsTargets>

The Details

I am aiming to replace the old behavior. Microsoft.Common.targets uses the MsTestToolsTargets property to define where to load the test tools .targets file from, so I have an easy way to replace the old behavior. Let’s first look at the existing Microsoft.TeamTest.targets file.

  <UsingTask TaskName="BuildShadowTask" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v9.0\TeamTest\Microsoft.VisualStudio.TestTools.BuildShadowsTask.dll"/>

If you are familiar with MSBuild project files, this is easy to understand. If not, just know that the UsingTask element is a way to import a coded MSBuild task. In this case, the BuildShadowTask being imported is what generates the accessor assembly. Well, sort of. It links to Publicize.exe, which does the actual assembly generation, but as far as MSBuild is concerned, the task’s implementation is irrelevant.

  <PropertyGroup>

    <ResolveReferencesDependsOn>

      $(ResolveReferencesDependsOn);

      ResolveTestReferences

    </ResolveReferencesDependsOn>
  </PropertyGroup>

In this section, the ResolveReferencesDependsOn property, which is used to determine the targets that the ResolveReferences target depends on, is modified to include the ResolveTestReferences target. The ResolveTestReferences target is the target that invokes BuildShadowTask, which you’ll see below. This is a standard way to add your custom targets to the common target dependency chain.

  <Target Name="ResolveTestReferences" Condition="'@(Shadow)'!=''">

    <BuildShadowTask

        CurrentResolvedReferences="@(ReferencePath)"

        CurrentCopyLocalFiles="@(ReferenceCopyLocalPaths)"

        Shadows="@(Shadow)"

        ProjectPath="$(ProjectDir)"

        IntermediatePath="$(IntermediateOutputPath)"

        SignAssembly="$(SignAssembly)"

        KeyFile="$(AssemblyOriginatorKeyFile)"

        DelaySign="$(DelaySign)">

      <Output TaskParameter="FixedUpReferences" ItemName="ReferencePath"/>

      <Output TaskParameter="NewCopyLocalAssemblies" ItemName="ReferenceCopyLocalPaths"/>

    </BuildShadowTask>
  </Target>

Here is the meat of the .targets file. It is a single target that leaves the bulk of the logic to the custom task. It passes all the relevant properties and items to the task, and assigns a couple of outputs. What you need to take from this is that when I re-implement this target, I need to maintain the same behavior. I looked at invoking Publicize.exe to accomplish this, but it turns out that Publicize.exe doesn’t have very many command line parameters. It could be done, but you’d need a custom task or a lot of MSBuild logic to accomplish the same as the standard BuildShadowTask. So, instead, I cheated. My solution was to shell out to MSBuild 3.5 to do this part of the build process. That means I need to be able to target the ResolveTestReferences target alone, which took a little extra work.

Let’s jump into the new .targets file. Remember, this replaces the above logic, it doesn’t add onto it.

  <UsingTask TaskName="BuildShadowTask" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v9.0\TeamTest\Microsoft.VisualStudio.TestTools.BuildShadowsTask.dll"/>

Here I didn’t make any changes to the orginal .targets file. I import the BuildShadowTask as before. I will, in fact, use it, just in a round-about way (you’ll see what I mean in a bit). 

  <PropertyGroup>

    <CoreResolveTestReferencesTarget Condition=" '$(CoreResolveTestReferencesTarget)' == '' ">ExternResolveTestReferences</CoreResolveTestReferencesTarget>

  </PropertyGroup>

Here is where some of the magic happens. Remember that I said my solution is to shell out to MSBuild 3.5. Well, the .targets file that I’m creating will be included in both the MSBuild 4.0 and MSBuild 3.5 cases. That is, the initial build and the shelled out build will both use this same logic. I differentiate the two scenarios in this property. The condition on the property states “if the property isn’t set”. That is, I only set the property if it wasn’t previously set. In this case, it won’t be set when the .targets file is loaded from MSBuild 4.0. When I shell out to MSBuild 3.5, I’ll set it explicitly. From MSBuild 4.0 I’ll set it to ExternResolveTestReferences, but when it is called from MSBuild 3.5 it will be set to CoreResolveTestReferences.

  <PropertyGroup>

    <ResolveReferencesDependsOn>

      $(ResolveReferencesDependsOn);

      $(CoreResolveTestReferencesTarget)

    </ResolveReferencesDependsOn>

  </PropertyGroup>

Like the original .targets file, I needed to fit my targets into the dependency chain for the common build targets. We add the target in the value of the property CoreResolveTestReferencesTarget, which I defined above.

  <PropertyGroup>
    <ResolveTestReferencesDependsOn>
      ResolveReferences;
      $(CoreResolveTestReferencesTarget)
    </ResolveTestReferencesDependsOn>
  </PropertyGroup>

I also added a standard dependency property for the ResolveTestReferences target. This is because when you use the original .targets file, you can’t build just the ResolveTestReferences target, because it doesn’t define its dependencies. I needed to call the target directly, so I added the required dependencies.

  <Target Name="ResolveTestReferences" Condition="'@(Shadow)'!=''" DependsOnTargets="$(ResolveTestReferencesDependsOn)" />

The original ResolveTestReferences contained all the logic to call out to the BuildShadowTask. Since I need to use different logic between MSBuild 4.0 and MSBuild 3.5 I added the target dependencies and removed the logic. The condition just states that “if there are Shadow items”. In other words, only run the target if items are defined as Shadow, which are the test reference (.accessor) files.

  <Target Name="CoreResolveTestReferences" Condition="'@(Shadow)'!=''">

The CoreResolveTestReferences target is the one called from MSBuild 3.5. It has the same condition as the ResolveTestReferences target to avoid unnecessary invocations. This target will look a lot like the original ResolveTestReferences target. In fact the next bit is a direct copy:

    <BuildShadowTask

        CurrentResolvedReferences="@(ReferencePath)"

        CurrentCopyLocalFiles="@(ReferenceCopyLocalPaths)"

        Shadows="@(Shadow)"

        ProjectPath="$(ProjectDir)"

        IntermediatePath="$(IntermediateOutputPath)"

        SignAssembly="$(SignAssembly)"

        KeyFile="$(AssemblyOriginatorKeyFile)"

        DelaySign="$(DelaySign)">

      <Output TaskParameter="FixedUpReferences" ItemName="ReferencePath"/>

      <Output TaskParameter="NewCopyLocalAssemblies" ItemName="ReferenceCopyLocalPaths"/>

    </BuildShadowTask>

Just like the original ResolveTestReferences, a call out to the BuildShadowTask is done. The difference is in the next couple of lines:

    <WriteLinesToFile

        File="$(IntermediateOutputPath)ReferencePath.txt"

        Lines="@(ReferencePath)"

        Overwrite="true" />

    <WriteLinesToFile

        File="$(IntermediateOutputPath)ReferenceCopyLocalPaths.txt"

        Lines="@(ReferenceCopyLocalPaths)"
        Overwrite="true" />

All I’m doing here is writing the values of the ReferencePath and RefenceCopyLocalPaths items out to a text file. Note that there is a potential for data loss here, if the items have metadata attached to them. In my tests, I didn’t see any issues. The reason I write the values of these items out to a file is that I want to read them back into MSBuild 4.0 space. It was the easiest way I could think of to try to replicate the original behavior.

  </Target>

And, of course, this just closes the CoreResolveTestReferences target. Now onto the MSBuild 4.0 logic.

  <Target Name="ExternResolveTestReferences" Condition="'@(Shadow)'!=''">

The ExternResolveTestRefences target contains the logic for MSBuild 4.0 to shell out to MSBuild 3.5. It uses the same condition as seen on the ResolveTestRefences target.

    <Exec Command="&quot;$(MSBuildBinPath)\MSBuild.exe&quot; /t:ResolveTestReferences &quot;$(MSBuildProjectFullPath)&quot; /p:Configuration=&quot;$(Configuration)&quot; /p:Platform=&quot;$(Platform)&quot; /p:OutDir=$(OutDir) /p:CoreResolveTestReferencesTarget=CoreResolveTestReferences" />

Here is where we shell out to MSBuild 3.5. In MSBuild 4.0, I found that the MSBuildBinPath property is set to the target framework’s bin path, not the 4.0 bin path, so I was able to use that to invoke the correct version of MSBuild.exe.

If you are not familiar with the MSBuild command line arguments, the /t indicates the target or targets to run and /p defines a property. Since I set up the dependency chain for the ResolveTestRefences target, we can just tell MSBuild 3.5 to build that target. After the /t argument you’ll see that I pass the full path to the MSBuild project that is currently being built, which should be the unit test project. The rest of the arguments are properties that need set to maintain behavior. This is where you might find the need to do some customization. I’ll go over some of the properties and why I included them.

The Configuration property is the configuration that you want to build. A configuration is typically ‘Release’ or ‘Debug’ for .NET projects, but it can be customized. Anyway, here I just wanted to pass the currently building configuration on to MSBuild 3.5 so it builds to the right folders, etc.

The Platform property is the platform that you are targeting. For example, x86 or x64. For .NET projects, it will typically be ‘Any CPU’. Just like the Configuration property, I just wanted to maintain the same for MSBuild 3.5 since it often is used to construct output file paths, etc.

The OutDir property is set because it is a commonly overridden property for TFS Build builds. This problem was initially found from a customer that was trying to use TFS Beta 2 to build legacy projects. Well, TFS Build overrides the OutDir property in order to get all your projects to put their outputs into a single location. To get project references and such to resolve correctly, it is needed, although no outputs are placed there from my invocation of MSBuild 3.5.

Finally, the CoreResolveTestReferencesTarget is set to CoreResolveTestReferences. This is the key to making sure that the invoked MSBuild 3.5 uses the CoreResolveTestReferences target, which does the actual BuildShadowTask call. Don’t change this.

Ok, now the last thing to do is read back in those files that we wrote from the CoreResolveTestReferences task in MSBuild 3.5.

    <ReadLinesFromFile File="$(IntermediateOutputPath)ReferencePath.txt">

      <Output TaskParameter="Lines" ItemName="ReferencePath" />

    </ReadLinesFromFile>

    <ReadLinesFromFile File="$(IntermediateOutputPath)ReferenceCopyLocalPaths.txt">

      <Output TaskParameter="Lines" ItemName="ReferenceCopyLocalPaths" />
    </ReadLinesFromFile>

Simply, I read the lines right back into the properties that they were written from in MSBuild 3.5, but in MSBuild 4.0 space. With any luck, any other targets will have the expected values already in these item properties. This was the final key to maintaining the same behavior as the original ResolveTestReferences target.

    <Delete Files="$(IntermediateOutputPath)ReferencePath.txt" />
    <Delete Files="$(IntermediateOutputPath)ReferenceCopyLocalPaths.txt" />

For good measure, I go ahead and delete the intermediate files, since they mean nothing for incremental building.

  </Target>

And, of course, this ends the ExternResolveTestReferences target.

All that code is attached as Microsoft.TeamTest.4.targets. You’ll want to drop that file into your %ProgramFiles%\MSBuild\Microsoft\VisualStudio\v9.0\TeamTest folder. On an x64 system, put it in %ProgramFiles(x86)%\MSBuild\Microsoft\VisualStudio\v9.0\TeamTest.

Now, to get your unit test projects to pick up this .targets file instead of the original, you need to add one property to them. If you have a large project structure, then perhaps you have a single place that you can add it. If you have a large project structure and aren’t using .targets files for your common customizations (properties, custom build processes, etc) then I suggest you start looking into it.

The key is to get this property defined before you import Microsoft.CSharp.targets. If you are at all familiar with MSBuild project files, you won’t have too much trouble. For the rest of you, I’ll show you how to add it to a plain ol’ unit test project. Open the unit test project file (.csproj or .vbproj) in notepad or VS using the XML editor. Find the first set of properties, something like:

  <PropertyGroup>

    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>

    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>

    <ProductVersion>9.0.30729</ProductVersion>

    <SchemaVersion>2.0</SchemaVersion>

    <ProjectGuid>{9C571CAA-F9D2-4103-8B1E-5AB41E34C187}</ProjectGuid>

    <OutputType>Library</OutputType>

    <AppDesignerFolder>Properties</AppDesignerFolder>

    <RootNamespace>UnitTests</RootNamespace>

    <AssemblyName>UnitTests</AssemblyName>

    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>

    <FileAlignment>512</FileAlignment>

    <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>

  </PropertyGroup>

Add this property somewhere in that property group:

<MsTestToolsTargets>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v9.0\TeamTest\Microsoft.TeamTest.4.targets</MsTestToolsTargets>

Save the file, and you’re done. Go build from MSBuild 4.0. You should see that the generated accessor assemblies are linked to the 2.0 CLR and not the 4.0 CLR. You can verify by opening the assembly in ILDasm.exe. Open the MANIFEST node.

What you should see:

 .assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 2:0:0:0
}

What you should not see:

 .assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly extern mscorlib as mscorlib_2
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 2:0:0:0
}
Conclusion

You can in fact build 2.0 linked accessor assemblies using MSBuild 4.0 Beta 2. However, this is only a workaround. You may need to customize it to your environment or needs.

I hope this helps some people out. Please do leave feedback if you find errors or if you just want to give a big thumbs up (or down).

Downloads

https://cid-6f4c66b0ee56cd90.skydrive.live.com/self.aspx/Public/Building%20VS%202008%20Unit%20Test%20Projects%20in%20MSBuild%204.0%20^5Beta%202^6/Microsoft.TeamTest.4.zip