Skip to content
Draft
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
12 changes: 11 additions & 1 deletion src/BinaryCompatChecker/CommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ public class CommandLine

public bool ResolveFromFramework => ResolveFromGac || ResolveFromNetCore;

public bool CheckPerAppConfig { get; set; };

public bool OutputExpectedWarnings { get; set; }
public bool OutputNewWarnings { get; set; }
public bool OutputSummary { get; set; }
Expand Down Expand Up @@ -374,6 +376,13 @@ public bool Process(IList<string> arguments)
continue;
}

if (argName.Equals("checkPerAppConfig", StringComparison.OrdinalIgnoreCase))
{
CheckPerAppConfig = true;
arguments.Remove(arg);
continue;
}

if (argName.Equals("outputExpectedWarnings", StringComparison.OrdinalIgnoreCase))
{
OutputExpectedWarnings = true;
Expand Down Expand Up @@ -1059,6 +1068,7 @@ and you want to check the dependencies of one of them and the corresponding
-ignoreInterfaces Do not report missing interface implementations.
-doNotResolveFromGAC Do not resolve assemblies from GAC.
-doNotResolveFromNetCore Do not resolve assemblies from .NET runtime directories.
-checkPerAppConfig Run the check for each app.config separately.
-ivt Report internal API surface area consumed via InternalsVisibleTo.
-embeddedInteropTypes Report embedded interop types.
-intPtrCtors Report IntPtr constructors (Mono).
Expand Down Expand Up @@ -1087,4 +1097,4 @@ Use this to limit the output to only the information required.
Write(@" -replicateBindingRedirects <source.exe.config> <destination.exe.config>+", ConsoleColor.White);
WriteLine();
}
}
}
302 changes: 180 additions & 122 deletions src/BinaryCompatChecker/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,19 +246,185 @@ public CheckResult Check()

filesToVisit.UnionWith(fileQueue);

if (commandLine.CheckPerAppConfig && appConfigFilePaths.Count > 1)
{
Checker[] allResults = Task.WhenAll(
appConfigFilePaths.Select(appConfigFilePath =>
{
Queue<string> filesForSubCheck = new Queue<string>(fileQueue);
var task = Task.Run(() =>
{
var subChecker = new Checker(commandLine);
subChecker.Check(filesForSubCheck, [appConfigFilePath]);
return subChecker;
});
return task;
})).Result;

assembliesExamined.AddRange(allResults.SelectMany(r => r.assembliesExamined).Distinct());

foreach (var d in allResults.SelectMany(r => r.diagnostics))
{
bool[] reportedDiagnostics = new bool[allResults.Length];
for (int i = 0; i < allResults.Length; i++)
{
if (allResults[i].diagnostics.Contains(d))
{
reportedDiagnostics[i] = true;
}
}
if (reportedDiagnostics.All(x => x))
{
diagnostics.Add(d);
}
else
{
var diagnostic = $"{d}. Not handled by: {string.Join(", ", appConfigFilePaths
.Where((_, idx) => reportedDiagnostics[idx])
.Select(GetRelativePath))}";
diagnostics.Add(diagnostic);
}
}

ivtUsages.AddRange(
allResults.SelectMany(r => r.ivtUsages)
.DistinctBy(ivt => (ivt.ExposingAssembly, ivt.ConsumingAssembly, ivt.Member)));
}
else
{
Check(fileQueue, appConfigFilePaths);
}

List<string> reportLines = new();

foreach (var diagnostic in diagnostics.OrderBy(s => s))
{
var text = diagnostic.Replace('\r', ' ').Replace('\n', ' ');
text = text.Replace(", Culture=neutral", "");
reportLines.Add(text);
}

if (File.Exists(baselineFile))
{
var baseline = File.ReadAllLines(baselineFile);
if (!Enumerable.SequenceEqual(baseline, reportLines, StringComparer.OrdinalIgnoreCase))
{
if (commandLine.EnableDefaultOutput || commandLine.OutputSummary)
{
if (!commandLine.IsBatchMode)
{
string customPrompt = commandLine.CustomFailurePrompt != null ? $"{Environment.NewLine}{commandLine.CustomFailurePrompt}" : "";
WriteError($@"Binary compatibility check failed.{customPrompt}
The current report is different from the baseline file.
Baseline file: {baselineFile}
Report file: {reportFile}");
}
}

if (commandLine.IsBatchMode)
{
result.BaselineDiagnostics = baseline;
result.ActualDiagnostics = reportLines;
}
else
{
OutputDiff(commandLine, baseline, reportLines);
}

try
{
Directory.CreateDirectory(Path.GetDirectoryName(reportFile));
File.WriteAllLines(reportFile, reportLines);
}
catch (Exception ex)
{
WriteError(ex.Message);
}

result.Success = false;
}
else
{
if (commandLine.EnableDefaultOutput || commandLine.OutputSummary)
{
if (!commandLine.IsBatchMode)
{
WriteLine($"Binary compatibility report matches the baseline file.", ConsoleColor.Green);
}
}
}
}
else if (!File.Exists(reportFile))
{
if (reportLines.Count > 0)
{
Directory.CreateDirectory(Path.GetDirectoryName(reportFile));

// initial baseline creation mode
File.WriteAllLines(reportFile, reportLines);

if (commandLine.EnableDefaultOutput || commandLine.OutputSummary)
{
if (!commandLine.IsBatchMode)
{
WriteError("Binary compatibility check failed.");
if (!string.IsNullOrEmpty(commandLine.CustomFailurePrompt))
{
WriteError(commandLine.CustomFailurePrompt);
}

WriteError($"Wrote {reportFile}");
}
}

result.BaselineDiagnostics = Array.Empty<string>();
result.ActualDiagnostics = reportLines;

if (!commandLine.IsBatchMode)
{
OutputDiff(commandLine, Array.Empty<string>(), reportLines);
}

result.Success = false;
}
}

ListExaminedAssemblies(reportFile);

if (commandLine.ReportIVT)
{
WriteIVTReport(reportFile);

WriteIVTReport(
reportFile,
".ivt.roslyn.txt",
u => Framework.IsRoslynAssembly(u.ExposingAssembly) && !Framework.IsRoslynAssembly(u.ConsumingAssembly));
}

privateAssemblyCache?.Clear();
((CustomAssemblyResolver)resolver).Clear();

return result;
}

private void Check(Queue<string> fileQueue, IEnumerable<string> appConfigFilePaths)
{
foreach (var appConfigFilePath in appConfigFilePaths)
{
bool ignoreVersionMismatch = commandLine.IgnoreVersionMismatchForAppConfigs.Contains(Path.GetFileName(appConfigFilePath), StringComparer.OrdinalIgnoreCase);

if (commandLine.EnableDefaultOutput && !commandLine.IsBatchMode)
{
Write(appConfigFilePath, ConsoleColor.Magenta);
if (ignoreVersionMismatch)
lock (Console.Out)
{
Write(" - ignoring version mismatches", ConsoleColor.DarkMagenta);
}
Write(appConfigFilePath, ConsoleColor.Magenta);
if (ignoreVersionMismatch)
{
Write(" - ignoring version mismatches", ConsoleColor.DarkMagenta);
}

WriteLine();
WriteLine();
}
}

var appConfigFileName = Path.GetFileName(appConfigFilePath);
Expand Down Expand Up @@ -299,14 +465,17 @@ public CheckResult Check()

if (commandLine.EnableDefaultOutput && !commandLine.IsBatchMode)
{
Write(file);
Write($" {assemblyDefinition.Name.Version}", color: ConsoleColor.DarkCyan);
if (targetFramework != null)
lock (Console.Out)
{
Write($" {targetFramework}", color: ConsoleColor.DarkGreen);
}
Write(file);
Write($" {assemblyDefinition.Name.Version}", color: ConsoleColor.DarkCyan);
if (targetFramework != null)
{
Write($" {targetFramework}", color: ConsoleColor.DarkGreen);
}

WriteLine();
WriteLine();
}
}

if (Framework.IsNetFrameworkAssembly(assemblyDefinition))
Expand Down Expand Up @@ -397,117 +566,6 @@ void BuildClosure(IEnumerable<string> assemblies)
}
}
}

List<string> reportLines = new();

foreach (var diagnostic in diagnostics.OrderBy(s => s))
{
var text = diagnostic.Replace('\r', ' ').Replace('\n', ' ');
text = text.Replace(", Culture=neutral", "");
reportLines.Add(text);
}

if (File.Exists(baselineFile))
{
var baseline = File.ReadLines(baselineFile).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray();
if (!Enumerable.SequenceEqual(baseline, reportLines, StringComparer.OrdinalIgnoreCase))
{
if (commandLine.EnableDefaultOutput || commandLine.OutputSummary)
{
if (!commandLine.IsBatchMode)
{
string customPrompt = commandLine.CustomFailurePrompt != null ? $"{Environment.NewLine}{commandLine.CustomFailurePrompt}" : "";
WriteError($@"Binary compatibility check failed.{customPrompt}
The current report is different from the baseline file.
Baseline file: {baselineFile}
Report file: {reportFile}");
}
}

if (commandLine.IsBatchMode)
{
result.BaselineDiagnostics = baseline;
result.ActualDiagnostics = reportLines;
}
else
{
OutputDiff(commandLine, baseline, reportLines);
}

try
{
Directory.CreateDirectory(Path.GetDirectoryName(reportFile));
File.WriteAllLines(reportFile, reportLines);
}
catch (Exception ex)
{
WriteError(ex.Message);
}

result.Success = false;
}
else
{
if (commandLine.EnableDefaultOutput || commandLine.OutputSummary)
{
if (!commandLine.IsBatchMode)
{
WriteLine($"Binary compatibility report matches the baseline file.", ConsoleColor.Green);
}
}
}
}
else if (!File.Exists(reportFile))
{
if (reportLines.Count > 0)
{
Directory.CreateDirectory(Path.GetDirectoryName(reportFile));

// initial baseline creation mode
File.WriteAllLines(reportFile, reportLines);

if (commandLine.EnableDefaultOutput || commandLine.OutputSummary)
{
if (!commandLine.IsBatchMode)
{
WriteError("Binary compatibility check failed.");
if (!string.IsNullOrEmpty(commandLine.CustomFailurePrompt))
{
WriteError(commandLine.CustomFailurePrompt);
}

WriteError($"Wrote {reportFile}");
}
}

result.BaselineDiagnostics = Array.Empty<string>();
result.ActualDiagnostics = reportLines;

if (!commandLine.IsBatchMode)
{
OutputDiff(commandLine, Array.Empty<string>(), reportLines);
}

result.Success = false;
}
}

ListExaminedAssemblies(reportFile);

if (commandLine.ReportIVT)
{
WriteIVTReport(reportFile);

WriteIVTReport(
reportFile,
".ivt.roslyn.txt",
u => Framework.IsRoslynAssembly(u.ExposingAssembly) && !Framework.IsRoslynAssembly(u.ConsumingAssembly));
}

privateAssemblyCache?.Clear();
((CustomAssemblyResolver)resolver).Clear();

return result;
}

public void CheckAssemblyReferenceVersion(
Expand Down