MSBuild will not execute targets in parallel - msbuild

I am trying to understand why MSBuild does not execute targets in parallel. Given a simple test.proj:
<Project>
<ItemGroup>
<_Project Include="$(MSBuildThisFileFullPath)"
Targets="_T1" />
<_Project Include="$(MSBuildThisFileFullPath)"
Targets="_T2" />
</ItemGroup>
<Target Name="Build">
<MSBuild BuildInParallel="true"
Projects="#(_Project)"
Targets="%(_Project.Targets)" />
</Target>
<Target Name="_T1">
<Exec Command="ping -n 3 127.0.0.1" />
</Target>
<Target Name="_T2">
<Exec Command="ping -n 3 127.0.0.1" />
</Target>
</Project>
and command:
msbuild /m /t:Build test.proj
_T1 and _T2 targets are executed sequentially. I must be missing something really simple, but I cannot figure out what it is.

You are invoking batching.
The %(_Project.Targets) value to the Targets parameter is creating batches based on distinct values of the Targets metadata. There is a batch for _T1 and a batch for _T2.
The MSBuild task is being invoked twice in sequence, once for each batch.
See "MSBuild batching".
Try the following instead to pass multiple projects to one invocation of the MSBuild task:
<MSBuild BuildInParallel="true"
Projects="#(_Project)"
Targets="_T1" />
Update
The code above is not correct for the example scenario in the question. The MSBuild task recognizes that the two items in #(_Project) are duplicates and executes the project once. If two different project files were used they wouldn't be duplicates and both would be run.
The items in #(_Project) will also not be duplicates if they have different metadata. There are special Properties and AdditionalProperties metadata that can be used with the MSBuild task. See "Pass properties to projects".
In the example code below:
I added different Properties metadata to each project (and removed the Targets metadata).
I added a Ping target. The Ping target runs different targets based on the passed property of PingDependsOn.
<Project>
<ItemGroup>
<_Project Include="$(MSBuildThisFileFullPath)">
<Properties>PingDependsOn=_T1</Properties>
</_Project>
<_Project Include="$(MSBuildThisFileFullPath)">
<Properties>PingDependsOn=_T2</Properties>
</_Project>
</ItemGroup>
<Target Name="Build">
<MSBuild BuildInParallel="true"
Projects="#(_Project)"
Targets="Ping" />
</Target>
<Target Name="Ping" DependsOnTargets="$(PingDependsOn)" />
<Target Name="_T1">
<Exec Command="ping -n 3 127.0.0.1" />
</Target>
<Target Name="_T2">
<Exec Command="ping -n 3 127.0.0.1" />
</Target>
</Project>
The project can be run as
dotnet msbuild parallel.proj /v:n
or
msbuild parallel.proj /m
(The switches are different because the dotnet tool runs the embedded MSBuild as /v:m /m and the standalone MSBuild runs as /v:n /m:1. )
A slightly edited example of the resulting output shows interleaved lines of text from targets _T1 and _T2:
1>Project "parallel.proj" on node 1 (default targets).
1>Project "parallel.proj" (1) is building "parallel.proj" (1:2) on
node 1 (Ping target(s)).
1>_T1:
ping -n 3 127.0.0.1
1>Project "parallel.proj" (1) is building "parallel.proj" (1:3) on
node 2 (Ping target(s)).
1>_T2:
ping -n 3 127.0.0.1
1>_T1:
Pinging 127.0.0.1 with 32 bytes of data:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
1>_T2:
Pinging 127.0.0.1 with 32 bytes of data:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
1>_T1:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
1>_T2:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
1>_T1:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Ping statistics for 127.0.0.1:
Packets: Sent = 3, Received = 3, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 0ms, Maximum = 0ms, Average = 0ms
1>Done Building Project "parallel.proj" (Ping target(s)).
1>_T2:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Ping statistics for 127.0.0.1:
Packets: Sent = 3, Received = 3, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 0ms, Maximum = 0ms, Average = 0ms
1>Done Building Project "parallel.proj" (Ping target(s)).
1>Done Building Project "parallel.proj" (default targets).
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:02.13

Related

How to pass env-var to MSBuild Task and make it effective for all sub-process? (concrete code)

I have a question about passing environment variables to MSBuild Task. See my code below:
a.proj
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTargets="A1">
<Target Name="A0">
<Message Text="A0 start."/>
<MSBuild Projects="b.proj" Properties="myvar=000" />
<Message Text="A0 end."/>
</Target>
<Target Name="A1" DependsOnTargets="A0">
<Message Text="A1 start."/>
<MSBuild Projects="b.proj" Properties="myvar=111" />
<Message Text="A1 end."/>
</Target>
</Project>
b.proj
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="B">
<Message Text="B start."/>
<Exec command="c.bat" />
</Target>
</Project>
c.bat
echo [C]myvar=%myvar%
I hope that c.bat can see myvar's value passed from a.proj. My code above failed to do that.
Consider(Assume) a running <MSBuild> Task a process on the OS, then, I'd like to pre-set environment variable myvar for that process, so that its subprocesses(no matter how deep) can all see myvar's value.
How can I achieve that? Thank you.
When MSBuild starts, it maps the environment variables in its process to MSBuild Properties. For example the environment variable COMPUTERNAME is available in an MSBuild project as the $(COMPUTERNAME) property.
But Properties are not added to the environment variables.
The Exec task creates a new process. The new process will inherit the environment variables of the 'parent' process and Exec has a parameter for passing additional environment variable definitions that will either add to or override the existing environment variables. The parameter is EnvironmentVariables.
Modify your Exec task as follows:
<Exec command="c.bat" EnvironmentVariables="myvar=$(myvar)" />
Example
The following is a full example of the environment variable passing.
Given a file 'test.proj':
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="TestExecPassVar">
<MSBuild Projects="$(MSBuildThisFile)" Targets="ExecPassVar" Properties="myvar=000" />
<MSBuild Projects="$(MSBuildThisFile)" Targets="ExecPassVar" Properties="myvar=111" />
<MSBuild Projects="$(MSBuildThisFile)" Targets="ExecPassVar" />
</Target>
<Target Name="ExecPassVar">
<Exec Command="test.bat" EnvironmentVariables="myvar=$(myvar)" EchoOff="true" />
</Target>
</Project>
And given a file 'test.bat':
#echo running test.bat
#echo %myvar%
Running the command
set myvar=222
and then
msbuild test.proj
will produce output like the following (which has been edited to remove paths and other noise):
Project "test.proj" on node 1 (TestExecPassVar target(s)).
Project "test.proj" (1) is building "test.proj" (1:2) on node 1 (ExecPassVar target(s)).
ExecPassVar:
running test.bat
000
Done Building Project "test.proj" (ExecPassVar target(s)).
Project "test.proj" (1) is building "test.proj" (1:3) on node 1 (ExecPassVar target(s)).
ExecPassVar:
running test.bat
111
Done Building Project "test.proj" (ExecPassVar target(s)).
Project "test.proj" (1) is building "test.proj" (1:4) on node 1 (ExecPassVar target(s)).
ExecPassVar:
running test.bat
222
Done Building Project "test.proj" (ExecPassVar target(s)).
Done Building Project "test.proj" (TestExecPassVar target(s)).
The test project invokes its own ExecPassVar target three times. The first two times it passes Properties that define a $(myvar) property. The third time no property is passed and the $(myvar) property is undefined.
The Exec task uses the EnvironmentVariables parameter.
When the $(myvar) property is defined, the batch file sees a myvar environment variable with the passed value.
When the $(myvar) property is not defined, the EnvironmentVariables parameter value evaluates to "myvar=". This doesn't override or clear the value of myvar and the value of 222, that was set before the msbuild command, is seen by the batch file.

Provide multiple build configurations to MSBuild through arguments and have them build in parallel

I'm trying to setup a MSBuild .proj file that can have multiple build configurations passed in as arguments and then have the configs built in parallel. This is an example of what I want to do:
msbuild MultiConfigBuild.proj /m:8 /ds /property:Config=Debug+Release;Platform=x64
I have got it building multiple configurations but I'm unable to get them to build in parallel even with BuildInParallel="true" and /m:8
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Rebuild" ToolsVersion="4.0">
<ItemGroup>
<ConfigList Condition=" '#(ConfigList)' == '' and $(Config) != '' " Include="$(Config.Split('+'))" /><!-- parse all requested configurations into a list -->
<ConfigList Condition=" '#(ConfigList)' == '' " Include="Debug" /><!-- if no configurations were specified, default to Debug -->
</ItemGroup>
<!--
Build the project for each requested configuration. -->
<Target Name="Rebuild">
<MSBuild Projects="$(MSBuildProjectDirectory)\Application.sln" Targets="Rebuild" Properties="Configuration=%(ConfigList.Identity);Platform=x64" BuildInParallel="true" />
</Target>
</Project>
Using the above proj file and command line parameters I get this output for the node utilisation:
============================== Node Utilization (IDs represent configurations) ====================================================
Timestamp: 1 Duration Cumulative
-----------------------------------------------------------------------------------------------------------------------------------
636942946549229765: 0 0.201s 0.201s ####
636942946551243522: 1 0.006s 0.207s
636942946551303033: 2 0.354s 0.561s #######
636942946554839510: | 0.674s 1.235s #############
636942946561575184: | 0.048s 1.282s
636942946562051339: | 3.362s 4.645s ###################################################################
636942946595675132: | 0.508s 5.152s ##########
636942946600754168: 1 0.001s 5.153s
636942946600764083: 0 0.007s 5.160s
636942946600833525: 3 0.002s 5.163s
636942946600858339: 4 0.136s 5.299s ##
636942946602217371: | 0.420s 5.719s ########
636942946606418477: | 0.023s 5.742s
636942946606651600: | 2.443s 8.185s ################################################
636942946631079558: | 1.692s 9.877s #################################
636942946648003047: 0 0.000s 9.878s
-----------------------------------------------------------------------------------------------------------------------------------
Utilization: 100.0 Average Utilization: 100.0
It's building the 2 configurations in serial on the same node.
I have tried the solution given in this question and that does build the configurations in parallel, but I was not able to set it up to parse multiple configs passed in as arguments.
When you use the %(Item.Metadata) syntax you use item batching, which creates batches of similar metadata that are executed sequentially. So the MSBuild task is called 2 times.
Instead, you want to pass multiple projects to one MSBuild task which have different configuration. You can accomplish this by using batching to create an Item containing the project (/solution) file to build with additional metadata that the MSBuild task uses to set properties (AdditionalProperties) and then pass the set of these project items to a single MSBuild task:
<Target Name="Rebuild">
<ItemGroup>
<ProjectToBuild Include="$(MSBuildProjectDirectory)\Application.sln">
<AdditionalProperties>Configuration=%(ConfigList.Identity)</AdditionalProperties>
</ProjectToBuild>
</ItemGroup>
<MSBuild Projects="#(ProjectToBuild)" Targets="Rebuild" Properties="Platform=x64" BuildInParallel="true" />
</Target>

Why does MSBuild copy task not copy?

I am trying to copy binaries created by my build to a certain folder using the MSBuild Copy task. Unfortuately, the process fails silently. No error message is issued and no files are copied.
Here is the relevant part of my .csproj file:
<Target Name="CopyFilesForModule" AfterTargets="AfterBuild">
<CreateItem Include="bin\**\*.*" Exclude="bin\**\*.pdb;bin\**\*.xml">
<Output TaskParameter="Include" ItemName="MySourceFiles" />
</CreateItem>
<Copy SourceFiles="$(MySourceFiles)" DestinationFolder="Areas\KoobooModule7\bin">
<Output
TaskParameter="CopiedFiles"
ItemName="Changed" />
</Copy>
<Message Text="sourcefiles: #(MySourceFiles)" />
<Message Text="changed:#(Changed)" Importance="high" />
</Target>
As you can see I have already added messages for debug purposes. I call msbuild using the following command line:
"c:\Program Files (x86)\MSBuild\12.0\Bin\MSBuild.exe" /t:rebuild /verbosity:d
iag KoobooModule7.csproj > buildlog.txt
Here is the relevant excerpt from my build log:
Target "CopyFilesForModule: (TargetId:75)" in project "C:\Users\chris\Documents\Visual Studio 2013\Projects\KoobooModule7\KoobooModule7\KoobooModule7.csproj" (target "Build" depends on it):
Task "CreateItem" (TaskId:43)
Task Parameter:Include=bin\**\*.* (TaskId:43)
Task Parameter:
Exclude=
bin\**\*.pdb
bin\**\*.xml (TaskId:43)
Done executing task "CreateItem". (TaskId:43)
Task "Copy" (TaskId:44)
Task Parameter:DestinationFolder=Areas\KoobooModule7\bin (TaskId:44)
Done executing task "Copy". (TaskId:44)
Task "Message" (TaskId:45)
Task Parameter:Text=sourcefiles: bin\CookComputing.XmlRpcV2.dll;bin\DiffPlex.dll;bin\dotless.Core.dll;bin\DotNetOpenAuth.AspNet.dll;bin\DotNetOpenAuth.Core.dll;bin\DotNetOpenAuth.OAuth.Consumer.dll;bin\DotNetOpenAuth.OAuth.dll;bin\DotNetOpenAuth.OpenId.dll;bin\DotNetOpenAuth.OpenId.RelyingParty.dll;bin\HtmlAgilityPack.dll;bin\Ionic.Zip.Reduced.dll;bin\Kooboo.CMS.Account.dll;bin\Kooboo.CMS.Caching.dll;bin\Kooboo.CMS.Common.dll;bin\Kooboo.CMS.Common.Runtime.Dependency.Ninject.dll;bin\Kooboo.CMS.Content.dll;bin\Kooboo.CMS.Form.dll;bin\Kooboo.CMS.Membership.dll;bin\Kooboo.CMS.Search.dll;bin\Kooboo.CMS.Sites.dll;bin\Kooboo.CMS.Sites.TemplateEngines.Razor.dll;bin\Kooboo.CMS.Web.dll;bin\Kooboo.dll;bin\KoobooModule7.dll;bin\KoobooModule7.dll.config;bin\Lucene.Net.Contrib.Highlighter.dll;bin\Lucene.Net.dll;bin\Microsoft.Web.Infrastructure.dll;bin\Mono.Math.dll;bin\Newtonsoft.Json.dll;bin\Ninject.dll;bin\NuGet.Core.dll;bin\Org.Mentalis.Security.Cryptography.dll;bin\System.Net.Http.dll;bin\System.Web.Helpers.dll;bin\System.Web.Mvc.dll;bin\System.Web.Razor.dll;bin\System.Web.WebPages.Administration.dll;bin\System.Web.WebPages.Deployment.dll;bin\System.Web.WebPages.dll;bin\System.Web.WebPages.Razor.dll (TaskId:45)
sourcefiles: bin\CookComputing.XmlRpcV2.dll;bin\DiffPlex.dll;bin\dotless.Core.dll;bin\DotNetOpenAuth.AspNet.dll;bin\DotNetOpenAuth.Core.dll;bin\DotNetOpenAuth.OAuth.Consumer.dll;bin\DotNetOpenAuth.OAuth.dll;bin\DotNetOpenAuth.OpenId.dll;bin\DotNetOpenAuth.OpenId.RelyingParty.dll;bin\HtmlAgilityPack.dll;bin\Ionic.Zip.Reduced.dll;bin\Kooboo.CMS.Account.dll;bin\Kooboo.CMS.Caching.dll;bin\Kooboo.CMS.Common.dll;bin\Kooboo.CMS.Common.Runtime.Dependency.Ninject.dll;bin\Kooboo.CMS.Content.dll;bin\Kooboo.CMS.Form.dll;bin\Kooboo.CMS.Membership.dll;bin\Kooboo.CMS.Search.dll;bin\Kooboo.CMS.Sites.dll;bin\Kooboo.CMS.Sites.TemplateEngines.Razor.dll;bin\Kooboo.CMS.Web.dll;bin\Kooboo.dll;bin\KoobooModule7.dll;bin\KoobooModule7.dll.config;bin\Lucene.Net.Contrib.Highlighter.dll;bin\Lucene.Net.dll;bin\Microsoft.Web.Infrastructure.dll;bin\Mono.Math.dll;bin\Newtonsoft.Json.dll;bin\Ninject.dll;bin\NuGet.Core.dll;bin\Org.Mentalis.Security.Cryptography.dll;bin\System.Net.Http.dll;bin\System.Web.Helpers.dll;bin\System.Web.Mvc.dll;bin\System.Web.Razor.dll;bin\System.Web.WebPages.Administration.dll;bin\System.Web.WebPages.Deployment.dll;bin\System.Web.WebPages.dll;bin\System.Web.WebPages.Razor.dll (TaskId:45)
Done executing task "Message". (TaskId:45)
Task "Message" (TaskId:46)
Task Parameter:Text=changed: (TaskId:46)
Task Parameter:Importance=high (TaskId:46)
changed: (TaskId:46)
Done executing task "Message". (TaskId:46)
Done building target "CopyFilesForModule" in project "KoobooModule7.csproj".: (TargetId:75)
As you can see, the files to copy have been identified correctly while the copied files are empty. How can I find out why the copy process fails?
You're not referencing the MySourceFiles item in the copy task, but the MySourceFiles property.
<Copy SourceFiles="$(MySourceFiles)" DestinationFolder="Areas\KoobooModule7\bin">
Should be
<Copy SourceFiles="#(MySourceFiles)" DestinationFolder="Areas\KoobooModule7\bin">

MSBuild /m:4 fails because it builds the same project twice

My team has a large solution (~500 csproj's). We use VS2012, and build using TFS Build, which uses MSBuild 4. Currently we build serially, but we want to build in parallel (using msbuild /maxcpucount:4). However, when I try it on my 4-proc machine, I get a weird failure:
11:2>CSC : fatal error CS0042: Unexpected error creating debug information file 'C:\Common\obj\Debug\Common.PDB' -- 'C:\Common\obj\Debug\Common.pdb: The process cannot access the file because it is being used by another process. [C:\Common\Common.csproj]
Looking at the log, 2 msbuild nodes were trying to build that same csproj, and thus colliding on writing some output:
10>Project "C:\Utils\Utils.csproj" (10) is building "C:\Common\Common.csproj" (11) on node 4 (default targets).
46:2>Project "C:\Objects\Objects.csproj" (46:2) is building "C:\Common\Common.csproj" (11:2) on node 1 (default targets).
Why would MSBuild try to build the same project twice?
Cause: Someone was calling <MSBuild Projects="Common.csproj" Properties="..." />. Then, MSBuild thinks that it should build Common.csproj again with those different properties, and it happened to occur at the same time with the regular compilation of Common.csproj.
Fix: Call <MSBuild ... /> without those unneeded properties.
Test:
Common.targets
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Build">
<Message Importance="high" Text="Build in $(MSBuildThisFile)" />
</Target>
<Target Name="After" DependsOnTargets="Build">
<Message Importance="high" Text="After in $(MSBuildThisFile)" />
</Target>
</Project>
Other.targets
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Build">
<Message Importance="high" Text="Build in $(MSBuildThisFile)" />
<MSBuild Projects="common.targets" Targets="Build" /> <!-- regular builds -->
<MSBuild Projects="common.targets" <!-- custom invocation with properties -->
Targets="After"
Properties="myprop=myvalue"
/>
</Target>
</Project>
Run:
> msbuild other.targets /clp:verbosity=minimal
Build in other.targets
Build in common.targets
Build in common.targets <<<< Common.targets Build is invoked again
After in common.targets
And indeed, removing Properties="myprop=myvalue" solves the issue.
I found someone had added two project references (from the same project) and that apparently caused msbuild to build twice also.. something to watch out for

If my MSBuild script uses batching to "loop" on a target, do its dependencies batch as well?

See this MSDN post for some background: Using metadata to call a target multiple times with different parameters
The solution put forward was to use metadata in a Task's Output attribute to force the batching of the task.
It so happens that our build step is partitioned across a few targets. My requirement is for the dependencies to run sequentially in a sort of inner loop.
I tried the concept out with the following script:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="BuildSolutions"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="3.5">
<ItemGroup>
<BuildFile Include="myscript.txt">
<InstallerFileName>MyInstaller1.msi</InstallerFileName>
<CustomTwiddleBit>true</CustomTwiddleBit>
<OtherCustomTwiddleBit>false</OtherCustomTwiddleBit>
</BuildFile>
<BuildFile Include="myscript.txt">
<InstallerFileName>MyInstaller2.msi</InstallerFileName>
<CustomTwiddleBit>false</CustomTwiddleBit>
<OtherCustomTwiddleBit>true</OtherCustomTwiddleBit>
</BuildFile>
</ItemGroup>
<Target
Name="BuildInstallers"
Outputs="OutputDir\%(BuildFile.InstallerFileName)"
DependsOnTargets="Step3"
>
<Exec Command="echo %(BuildFile.InstallerFileName)" />
</Target>
<Target Name="Step1">
<Exec Command="echo step 1 %(BuildFile.InstallerFileName)" />
</Target>
<Target Name="Step2"
DependsOnTargets="Step1">
<Exec Command="echo step 2 %(BuildFile.InstallerFileName)" />
</Target>
<Target Name="Step3"
DependsOnTargets="Step2">
<Exec Command="echo step 3 %(BuildFile.InstallerFileName)" />
</Target>
</Project>
The output was as follows:
Microsoft (R) Build Engine Version 3.5.30729.1
[Microsoft .NET Framework, Version 2.0.50727.3053]
Copyright (C) Microsoft Corporation 2007. All rights reserved.
Build started 2/16/2009 20:41:14.
Project "D:\Source\test\test.proj" on node 0 (BuildInstallers target(s)).
step 1 MyInstaller1.msi
step 1 MyInstaller2.msi
Step2:
step 2 MyInstaller1.msi
step 2 MyInstaller2.msi
Step3:
step 3 MyInstaller1.msi
step 3 MyInstaller2.msi
BuildInstallers:
MyInstaller1.msi
BuildInstallers:
MyInstaller2.msi
Done Building Project "D:\Source\test\test.proj" (BuildInstallers target(s)).
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.43
Any ideas how I might get the output to be like so:
step 1 MyInstaller1.msi
step 2 MyInstaller1.msi
step 3 MyInstaller1.msi
step 1 MyInstaller2.msi
step 2 MyInstaller2.msi
step 3 MyInstaller2.msi
I sure see the problem here as
<Exec Command="echo step 1 %(BuildFile.InstallerFileName)" />
Will iterate again and again on the InstallerFileName output. I think you can call a custom target instead and that target go through each of your steps. Call this target from the main script with the MSBuild Task, something like this:
<MSBuild Projects="CustomTargets.proj"
Targets="BuildInstallers"
Properties="BuildFile=%(BuildFile.InstallerFileName)" />
And in you BuildInstallers target you would use
<Target ="BuildInstallers" DependsOnTargets="Step3" >
</Target>
<Target Name="Step1">
<Exec Command="echo step 1 $(BuildFile)" />
</Target>
etc
When you're defining targets and dependencies, you're defining a dependency tree. MSBuild is at liberty to execute the targets in any order it sees fit, including in parallel (when using /m), as long as the order doesn't violate any dependencies.
In your case, MSBuild chose to run step 1 MyInstaller1.msi + step 1 MyInstaller2.msi first, and if you'd add /m, they'll probably run in parallel. If they can't run in parallel, that's probably a limitation in your design - probably sharing the same intermediate files - and you should fix that.
If you want the output ordered like that for performance reasons that you can't explain to MSBuild, I guess you can use the <MSBuild> hack described above.