Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,16 @@
</When>
</Choose>

</Project>
<!--
Explicitly reference System.Collections.Immutable on .NET Framework for all non-test product
projects. This overrides the older version pulled in transitively by System.Reflection.Metadata
8.0.0 and ensures every net462 assembly in the shipped nupkg and V2.CLI VSIX references the
same SCI version as the SCI DLL we ship next to it. Required to fix FileLoadException in hosts
without binding redirects (e.g. Azure DevOps Distributed Test Agent) and to keep DLLs in the
VSIX-style Common7/IDE/Extensions/TestPlatform/Extensions/ layout consistent with each other.
-->
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework' AND '$(IsTestProject)' != 'true' AND !$(MSBuildProjectDirectory.Replace('\','/').Contains('/src/package/'))">
<PackageReference Include="System.Collections.Immutable" Version="$(SystemCollectionsImmutableVersion)" />
</ItemGroup>

</Project>
211 changes: 211 additions & 0 deletions eng/verify-binding-redirects.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest

$script:isCI = $env:TF_BUILD -eq 'true' -or $env:CI -eq 'true'

# Verifies that binding redirects in source app.config files match the actual
# assembly versions of the DLLs shipped in the extracted nupkg packages.
#
# In CI: validates and fails with instructions to run locally.
# Locally: auto-fixes the source app.config files with the correct versions.

# Each source app.config maps to a specific exe that ships in the packages.
$script:AppConfigs = @(
@{ Config = "src/vstest.console/app.config"; ExeName = "vstest.console.exe" }
@{ Config = "src/testhost.x86/app.config"; ExeName = "testhost.x86.exe" }
@{ Config = "src/datacollector/app.config"; ExeName = "datacollector.exe" }
)

function Find-ExeInPackages {
param(
[string] $ExeName,
[string[]] $PackageDirs
)

foreach ($dir in $PackageDirs) {
$found = Get-ChildItem $dir -Recurse -Filter $ExeName -File -ErrorAction SilentlyContinue
if ($found) {
# Prefer the one closest to a net462 or root layout (not nested in TestHostNetFramework).
$preferred = $found | Where-Object { $_.FullName -notlike "*TestHostNetFramework*" } | Select-Object -First 1
if ($preferred) { return $preferred.DirectoryName }
return $found[0].DirectoryName
}
}

return $null
}

function Get-ManagedAssemblyVersion {
param([string] $DllPath)

try {
return [System.Reflection.AssemblyName]::GetAssemblyName($DllPath).Version.ToString()
}
catch {
return $null
}
}

function Verify-BindingRedirects {
param(
[Parameter(Mandatory)]
[string[]]$PackageDirs,

[Parameter(Mandatory)]
[ValidateSet("Debug", "Release")]
[string]$Configuration
)

$repoRoot = Resolve-Path "$PSScriptRoot/.."
$errors = @()
$configsToFix = @{}

foreach ($entry in $script:AppConfigs) {
$configPath = Join-Path $repoRoot $entry.Config
if (-not (Test-Path $configPath)) {
Write-Host "Skipping $($entry.ExeName): config '$configPath' not found."
continue
}

$deployDir = Find-ExeInPackages -ExeName $entry.ExeName -PackageDirs $PackageDirs
if (-not $deployDir) {
Write-Host "Skipping $($entry.ExeName): not found in any extracted package."
continue
}

Write-Host "Checking assembly redirects for $($entry.ExeName) (from '$deployDir')..."

[xml]$xml = Get-Content $configPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("asm", "urn:schemas-microsoft-com:asm.v1")

# Build search directories from probing paths in the config.
$searchDirs = @($deployDir)
$probingNodes = $xml.SelectNodes("//asm:probing", $nsMgr)
foreach ($probing in $probingNodes) {
$privatePath = $probing.GetAttribute("privatePath")
if ($privatePath) {
foreach ($subPath in $privatePath -split ";") {
$probingDir = Join-Path $deployDir $subPath.Trim()
if (Test-Path $probingDir) { $searchDirs += $probingDir }
}
}
}

$dependentAssemblies = $xml.SelectNodes("//asm:dependentAssembly", $nsMgr)
foreach ($dep in $dependentAssemblies) {
$identity = $dep.SelectSingleNode("asm:assemblyIdentity", $nsMgr)
$redirect = $dep.SelectSingleNode("asm:bindingRedirect", $nsMgr)
if (-not $identity -or -not $redirect) { continue }

$assemblyName = $identity.GetAttribute("name")
$currentNewVersion = $redirect.GetAttribute("newVersion")
$currentOldVersion = $redirect.GetAttribute("oldVersion")

# Look for the assembly DLL in each search directory.
$dllPath = $null
foreach ($dir in $searchDirs) {
$candidate = Join-Path $dir "$assemblyName.dll"
if (Test-Path $candidate) { $dllPath = $candidate; break }
}

if (-not $dllPath) {
Write-Host " $assemblyName - not found in package layout, skipping."
continue
}

$actualVersion = Get-ManagedAssemblyVersion -DllPath $dllPath
if (-not $actualVersion) {
Write-Host " $assemblyName - could not read version (native or corrupt?), skipping."
continue
}

if ($currentNewVersion -eq $actualVersion) {
Write-Host " $assemblyName - OK ($actualVersion)"
continue
}

# newVersion needs updating, and the upper bound of oldVersion range too.
$newOldVersion = $currentOldVersion
if ($currentOldVersion -match '^(.*)-(.*)$') {
$newOldVersion = "$($Matches[1])-$actualVersion"
}

$errors += "$($entry.ExeName): $assemblyName redirect newVersion is '$currentNewVersion' but actual assembly version is '$actualVersion'"

if (-not $configsToFix.ContainsKey($configPath)) {
$configsToFix[$configPath] = @()
}

$configsToFix[$configPath] += @{
AssemblyName = $assemblyName
OldNewVersion = $currentNewVersion
NewNewVersion = $actualVersion
OldOldVersion = $currentOldVersion
NewOldVersion = $newOldVersion
}

if ($script:isCI) {
Write-Host " $assemblyName - MISMATCH: expected $actualVersion, found $currentNewVersion" -ForegroundColor Red
}
else {
Write-Host " $assemblyName - FIXING: $currentNewVersion -> $actualVersion (oldVersion: $currentOldVersion -> $newOldVersion)" -ForegroundColor Yellow
}
}
}

# Apply fixes using XML DOM with whitespace preservation.
if (-not $script:isCI) {
foreach ($configPath in $configsToFix.Keys) {
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($configPath)

$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("asm", "urn:schemas-microsoft-com:asm.v1")

foreach ($fix in $configsToFix[$configPath]) {
$nodes = $xmlDoc.SelectNodes("//asm:dependentAssembly[asm:assemblyIdentity[@name='$($fix.AssemblyName)']]/asm:bindingRedirect", $nsMgr)
$applied = $false
foreach ($node in $nodes) {
if ($node.GetAttribute("newVersion") -eq $fix.OldNewVersion) {
$node.SetAttribute("oldVersion", $fix.NewOldVersion)
$node.SetAttribute("newVersion", $fix.NewNewVersion)
$applied = $true
}
}

if (-not $applied) {
Write-Error "Failed to apply binding redirect fix for '$($fix.AssemblyName)' in '$configPath'. The expected redirect node was not found."
}
}

# Preserve the original BOM if present.
$bom = [System.IO.File]::ReadAllBytes($configPath)
$hasBom = $bom.Length -ge 3 -and $bom[0] -eq 0xEF -and $bom[1] -eq 0xBB -and $bom[2] -eq 0xBF
$encoding = if ($hasBom) { New-Object System.Text.UTF8Encoding($true) } else { New-Object System.Text.UTF8Encoding($false) }
$writer = New-Object System.IO.StreamWriter($configPath, $false, $encoding)
$xmlDoc.Save($writer)
$writer.Dispose()
Write-Host "Updated '$configPath'." -ForegroundColor Green
}
}

if ($errors) {
if ($script:isCI) {
$message = "Assembly binding redirect mismatches detected:`n"
$message += ($errors -join "`n")
$message += "`n`nTo fix this, run the following command locally after building and packing:`n"
$message += " .\build.cmd -c $Configuration`n"
$message += "This will rebuild, pack, and auto-update the app.config files with the correct versions.`n"
$message += "Then commit the updated app.config files."
Write-Error $message
}
else {
Write-Host "`nFixed $($errors.Count) binding redirect(s). Please commit the updated app.config files." -ForegroundColor Green
}
}
else {
Write-Host "All binding redirects match their DLL versions."
}
}
6 changes: 6 additions & 0 deletions eng/verify-nupkgs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Param(
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.IO.Compression.FileSystem

# Import binding redirect verification.
. "$PSScriptRoot/verify-binding-redirects.ps1"

function Verify-Nuget-Packages {
Write-Host "Starting Verify-Nuget-Packages."
$expectedNumOfFiles = @{
Expand Down Expand Up @@ -316,3 +319,6 @@ Start-sleep -Seconds 10
# skipped, it is hard to find the right dumpbin.exe and corflags tools on server
# Verify-NugetPackageExe -configuration $configuration -UnzipNugetPackages $unzipNugetPackages
Verify-NugetPackageVersion -configuration $configuration -UnzipNugetPackages $unzipNugetPackages

Write-Host "`nVerifying binding redirects..."
Verify-BindingRedirects -PackageDirs $unzipNugetPackages -Configuration $configuration
4 changes: 2 additions & 2 deletions src/datacollector/app.config
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />
Expand Down Expand Up @@ -26,7 +26,7 @@
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="1.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
<bindingRedirect oldVersion="1.0.0.0-9.0.0.11" newVersion="9.0.0.11" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reflection.Metadata" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
<MicrosoftExtensionsFileSystemGlobbing Include="$(PkgMicrosoft_Extensions_FileSystemGlobbing)\lib\netstandard2.0\*" />
<NewtonsoftJson Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*" />
<SystemCollectionsImmutable Include="$(PkgSystem_Collections_Immutable)\lib\netstandard2.0\*" />
<!-- See note in Microsoft.TestPlatform.csproj: ship net462 SCI (AV 9.0.0.11) alongside Common.dll
at the package root, while keeping netstandard2.0 (9.0.0.0) in nested Extensions subfolders. -->
<SystemCollectionsImmutableNet462 Include="$(PkgSystem_Collections_Immutable)\lib\net462\System.Collections.Immutable.dll" Condition="'$(PkgSystem_Collections_Immutable)' != ''" />
<SystemReflectionMetadata Include="$(PkgSystem_Reflection_Metadata)\lib\netstandard2.0\*" />
<MicrosoftInternalDia Include="$(PkgMicrosoft_Internal_Dia)\tools\net451\**\*" />
</ItemGroup>
Expand All @@ -95,6 +98,7 @@
<Copy SourceFiles="@(MicrosoftExtensionsFileSystemGlobbing)" DestinationFiles="$(OutDir)\%(RecursiveDir)%(Filename)%(Extension)" />
<Copy SourceFiles="@(NewtonsoftJson)" DestinationFiles="$(OutDir)\%(RecursiveDir)%(Filename)%(Extension)" />
<Copy SourceFiles="@(SystemCollectionsImmutable)" DestinationFiles="$(OutDir)\%(RecursiveDir)%(Filename)%(Extension)" />
<Copy SourceFiles="@(SystemCollectionsImmutableNet462)" DestinationFiles="$(OutDir)\SCI_Root\%(Filename)%(Extension)" />
<Copy SourceFiles="@(SystemReflectionMetadata)" DestinationFiles="$(OutDir)\%(RecursiveDir)%(Filename)%(Extension)" />
<Copy SourceFiles="@(MicrosoftInternalDia)" DestinationFiles="$(OutDir)\Microsoft.Internal.Dia\%(RecursiveDir)%(Filename)%(Extension)" />
</Target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<file src="net48\zh-Hant\Microsoft.CodeCoverage.IO.resources.dll" target="tools\net462\zh-Hant\Microsoft.CodeCoverage.IO.dll" />

<file src="net48\Newtonsoft.Json.dll" target="tools\net462\Newtonsoft.Json.dll" />
<file src="net48\System.Collections.Immutable.dll" target="tools\net462\System.Collections.Immutable.dll" />
<file src="net48\SCI_Root\System.Collections.Immutable.dll" target="tools\net462\System.Collections.Immutable.dll" />

<file src="net48\System.Reflection.Metadata.dll" target="tools\net462\System.Reflection.Metadata.dll" />
<file src="net48\System.Memory.dll" target="tools\net462\System.Memory.dll" />
Expand Down
12 changes: 11 additions & 1 deletion src/package/Microsoft.TestPlatform/Microsoft.TestPlatform.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- $(NetFrameworkRunnerTargetFramework); should be added here, but it causes duplicates in tfm names and nuget restore fails -->
<TargetFrameworks>$(TestHostAllTargetFrameworks)</TargetFrameworks>
Expand Down Expand Up @@ -107,6 +107,15 @@
<PackageDepsJsonFiles Include="..\..\Microsoft.TestPlatform.PlatformAbstractions\bin\$(Configuration)\$(TargetFramework)\*.deps.json"></PackageDepsJsonFiles>
<NewtonsoftJsonFiles Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*"></NewtonsoftJsonFiles>
<SystemCollectionsImmutableFiles Include="$(PkgSystem_Collections_Immutable)\lib\netstandard2.0\*"></SystemCollectionsImmutableFiles>
<!--
The net462 build of SCI has AssemblyVersion 9.0.0.11, which differs from the other TFMs' 9.0.0.0.
We compile Common.dll and friends against the net462 SCI facade (via the central PackageReference
in Directory.Build.targets), so the net462 flavor of SCI must ship next to them in the package
ROOT (tools/net462/Common7/IDE/Extensions/TestPlatform/) for DTA-style hosts that do not get
binding redirects injected. The netstandard2.0 flavor (9.0.0.0) is still used in the nested
Extensions\ subfolder to avoid conflicting-version loads in .NET 9 acceptance-test processes.
-->
<SystemCollectionsImmutableNet462Files Include="$(PkgSystem_Collections_Immutable)\lib\net462\System.Collections.Immutable.dll"></SystemCollectionsImmutableNet462Files>
<SystemReflectionMetadataFiles Include="$(PkgSystem_Reflection_Metadata)\lib\netstandard2.0\*"></SystemReflectionMetadataFiles>
<MicrosoftInternalDiaFiles Include="$(PkgMicrosoft_Internal_Dia)\tools\net451\**\*"></MicrosoftInternalDiaFiles>
<MicrosoftInternalIntellitraceFiles Include="$(PkgMicrosoft_Internal_Intellitrace)\tools\net451\**\*"></MicrosoftInternalIntellitraceFiles>
Expand All @@ -127,6 +136,7 @@
<Copy SourceFiles="@(PlatformAbstractionsDepsJsonFiles)" DestinationFiles="$(OutDir)\%(RecursiveDir)%(Filename)%(Extension)" />
<Copy SourceFiles="@(NewtonsoftJsonFiles)" DestinationFiles="$(OutDir)\%(RecursiveDir)%(Filename)%(Extension)" />
<Copy SourceFiles="@(SystemCollectionsImmutableFiles)" DestinationFiles="$(OutDir)\%(RecursiveDir)%(Filename)%(Extension)" />
<Copy SourceFiles="@(SystemCollectionsImmutableNet462Files)" DestinationFiles="$(OutDir)\SCI_Root\%(Filename)%(Extension)" />
<Copy SourceFiles="@(SystemReflectionMetadataFiles)" DestinationFiles="$(OutDir)\%(RecursiveDir)%(Filename)%(Extension)" />
<Copy SourceFiles="@(MicrosoftInternalDiaFiles)" DestinationFiles="$(OutDir)\Microsoft.Internal.Dia\%(RecursiveDir)%(Filename)%(Extension)" />
<Copy SourceFiles="@(MicrosoftInternalIntellitraceFiles)" DestinationFiles="$(OutDir)\Microsoft.Internal.Intellitrace\%(RecursiveDir)%(Filename)%(Extension)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@

<!-- This is needed by System.Reflection.Metadata and its dependencies. -->
<file src="net48\System.Reflection.Metadata.dll" target="tools\net462\Common7\IDE\Extensions\TestPlatform\System.Reflection.Metadata.dll" />
<file src="net48\System.Collections.Immutable.dll" target="tools\net462\Common7\IDE\Extensions\TestPlatform\System.Collections.Immutable.dll" />
<file src="net48\SCI_Root\System.Collections.Immutable.dll" target="tools\net462\Common7\IDE\Extensions\TestPlatform\System.Collections.Immutable.dll" />
<file src="net48\System.Memory.dll" target="tools\net462\Common7\IDE\Extensions\TestPlatform\System.Memory.dll" />
<file src="net48\System.Runtime.CompilerServices.Unsafe.dll" target="tools\net462\Common7\IDE\Extensions\TestPlatform\System.Runtime.CompilerServices.Unsafe.dll" />
<file src="net48\System.Numerics.Vectors.dll" target="tools\net462\Common7\IDE\Extensions\TestPlatform\System.Numerics.Vectors.dll" />
Expand Down
4 changes: 2 additions & 2 deletions src/testhost.x86/app.config
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />
Expand Down Expand Up @@ -39,7 +39,7 @@
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="1.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
<bindingRedirect oldVersion="1.0.0.0-9.0.0.11" newVersion="9.0.0.11" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reflection.Metadata" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
Expand Down
4 changes: 2 additions & 2 deletions src/vstest.console/app.config
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />
Expand Down Expand Up @@ -31,7 +31,7 @@
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="1.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
<bindingRedirect oldVersion="1.0.0.0-9.0.0.11" newVersion="9.0.0.11" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reflection.Metadata" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
Expand Down
Loading
Loading