Skip to content

Commit 5e4d30c

Browse files
JanProvaznikCopilot
andcommitted
Fix net472 out-of-proc tests crashing on servicing binding-redirect skew
Test projects that re-bundle MSBuild.exe (via ProjectReference to MSBuild.csproj) copy the servicing MSBuild.exe.config but resolve their own copy-local runtime assemblies at the compiled-against versions. The redirects then point at servicing versions (e.g. System.Memory 4.0.5.0) while the on-disk assembly is the compiled-against one (4.0.2.0), so the copied MSBuild.exe fails to start with a TypeInitializationException/FileLoadException. This broke every net472 out-of-proc test and hung tests waiting on a spawned node/server until the CI timeout. Add an AfterTargets="Build" target (net472, non-deploy projects only) that realigns the copied MSBuild.exe.config binding redirects to the runtime assemblies actually present next to it, keeping those bins self-consistent at the compiled-against versions. Deploy surfaces (MSBuild.exe, bootstrap) are excluded since their output already is the servicing build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 910e1b3 commit 5e4d30c

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

src/Directory.Build.targets

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,106 @@
296296
</ItemGroup>
297297
</Target>
298298

299+
<!--
300+
The net472 servicing redistribution above swaps DLLs and updates the binding redirects only in
301+
the projects that opt in ($(DeployRuntimeServicingAssemblies) == 'true': MSBuild.exe and the
302+
bootstrap). Other net472 projects that re-bundle MSBuild.exe (the test projects that spawn it
303+
out-of-proc, e.g. via ProjectReference to MSBuild.csproj) copy MSBuild.exe.config (which now
304+
carries the servicing redirects) but resolve their own copy-local runtime assemblies at the
305+
compiled-against versions. That leaves MSBuild.exe.config pointing at servicing versions
306+
(e.g. System.Memory 4.0.5.0) while the assembly on disk is the compiled-against one
307+
(4.0.2.0), so the copied MSBuild.exe fails to start with a FileLoadException, breaking every
308+
out-of-proc test (and hanging tests that wait on a spawned node/server).
309+
310+
These bins are not a VS redistribution surface, so the fix is to keep them self-consistent at
311+
the compiled-against versions: realign the copied MSBuild.exe.config binding redirects to the
312+
runtime assemblies actually next to it. Deploy surfaces are excluded; their DLLs already are
313+
the servicing build, so realigning would be a no-op there anyway.
314+
-->
315+
<UsingTask TaskName="AlignBindingRedirectsToOutputAssemblies"
316+
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll"
317+
TaskFactory="RoslynCodeTaskFactory">
318+
<ParameterGroup>
319+
<ConfigFiles ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
320+
</ParameterGroup>
321+
<Task>
322+
<Code Type="Fragment" Language="cs"><![CDATA[
323+
System.Xml.Linq.XNamespace ns = "urn:schemas-microsoft-com:asm.v1";
324+
foreach (Microsoft.Build.Framework.ITaskItem item in ConfigFiles)
325+
{
326+
string configPath = item.ItemSpec;
327+
if (!System.IO.File.Exists(configPath))
328+
{
329+
continue;
330+
}
331+
332+
string directory = System.IO.Path.GetDirectoryName(configPath);
333+
System.Xml.Linq.XDocument document = System.Xml.Linq.XDocument.Load(configPath);
334+
bool changed = false;
335+
336+
foreach (System.Xml.Linq.XElement dependentAssembly in document.Descendants(ns + "dependentAssembly"))
337+
{
338+
System.Xml.Linq.XElement identity = dependentAssembly.Element(ns + "assemblyIdentity");
339+
System.Xml.Linq.XElement redirect = dependentAssembly.Element(ns + "bindingRedirect");
340+
if (identity == null || redirect == null)
341+
{
342+
continue;
343+
}
344+
345+
string name = (string)identity.Attribute("name");
346+
if (string.IsNullOrEmpty(name))
347+
{
348+
continue;
349+
}
350+
351+
string assemblyPath = System.IO.Path.Combine(directory, name + ".dll");
352+
if (!System.IO.File.Exists(assemblyPath))
353+
{
354+
continue;
355+
}
356+
357+
string version;
358+
try
359+
{
360+
version = System.Reflection.AssemblyName.GetAssemblyName(assemblyPath).Version.ToString();
361+
}
362+
catch (System.Exception)
363+
{
364+
continue;
365+
}
366+
367+
redirect.SetAttributeValue("oldVersion", "0.0.0.0-" + version);
368+
redirect.SetAttributeValue("newVersion", version);
369+
370+
System.Xml.Linq.XElement codeBase = dependentAssembly.Element(ns + "codeBase");
371+
if (codeBase != null)
372+
{
373+
codeBase.SetAttributeValue("version", version);
374+
}
375+
376+
changed = true;
377+
}
378+
379+
if (changed)
380+
{
381+
document.Save(configPath);
382+
Log.LogMessage(Microsoft.Build.Framework.MessageImportance.Normal, "Aligned binding redirects in '{0}' to the runtime assemblies in its output directory.", configPath);
383+
}
384+
}
385+
]]></Code>
386+
</Task>
387+
</UsingTask>
388+
389+
<Target Name="AlignCopiedMSBuildExeConfigBindingRedirects"
390+
AfterTargets="Build"
391+
Condition="'$(TargetFramework)' == 'net472' and '$(DeployRuntimeServicingAssemblies)' != 'true'">
392+
<ItemGroup>
393+
<_CopiedMSBuildExeConfig Include="$(TargetDir)**\MSBuild.exe.config" />
394+
</ItemGroup>
395+
<AlignBindingRedirectsToOutputAssemblies ConfigFiles="@(_CopiedMSBuildExeConfig)"
396+
Condition="'@(_CopiedMSBuildExeConfig)' != ''" />
397+
</Target>
398+
299399
<!-- Import parent targets -->
300400
<Import Project="..\Directory.Build.targets"/>
301401

0 commit comments

Comments
 (0)