I spent some of my weekend determining whether the IL changed between builds. The following script builds a solution, generates intermediate language from the build result, filters out stuff that always changes (such as the MVID), and creates a hash of the result. If two hashes are the same, then the two builds resulted in functionally identical results. This is useful for verifying automated formatting.
function Get-IntermediateLanguageHash ([string]$SolutionPath) {
$timestamp = (Get-Date).Ticks;
$msbuildOutputPath = "$env:TMP/ByteCodeHash.$timestamp";
Write-Host "Building $SolutionPath to $msbuildOutputPath";
New-Item -ItemType Directory $msbuildOutputPath | Out-Host;
# Build the solution.
msbuild $SolutionPath `
-p:Platform=x64 `
-p:Configuration=Release `
-p:OutputPath=$msbuildOutputPath `
-verbosity:minimal `
-restore | Out-Host;
$dotNetBinaries = Get-ChildItem $msbuildOutputPath -File -Recurse `
| Where-Object { $_.Extension -match "exe|dll" } `
| Select-Object -ExpandProperty FullName;
Write-Host "Hashing IL output of $($dotNetBinaries.Count) files in $msbuildOutputPath.";
# Covert each to IL.
$ildasmExe = "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\ildasm.exe";
$intermediateLanguageFilePaths = $dotNetBinaries | ForEach-Object {
$inputPath = $_;
$ildasmOutputPath = "$($inputPath).il.filtered";
# If there is an error, we probably have a .NET Core executable.
# See https://stackoverflow.com/a/60590779/1108891
. $ildasmExe $inputPath /text /utf8 2>$null `
| Where-Object {
$_ -notmatch '// Image base: 0x' `
-and $_ -notmatch $timestamp `
-and $_ -notmatch '// MVID:';
} `
| Add-Content $ildasmOutputPath;
return $ildasmOutputPath;
}
# Hash the content.
$stringAsStream = [System.IO.MemoryStream]::new();
$writer = [System.IO.StreamWriter]::new($stringAsStream);
Get-Content $intermediateLanguageFilePaths `
| ForEach-Object {
$line = $_;
$writer.Write($line);
}
$writer.Flush();
$stringAsStream.Position = 0;
$result = Get-FileHash -InputStream $stringAsStream;
return $result.Hash;
}