// Copyright (c) Ubisoft. All Rights Reserved. // Licensed under the Apache 2.0 License. See LICENSE.md in the project root for license information. using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; using Sharpmake.Generators; using Sharpmake.Generators.VisualStudio; namespace Sharpmake.Application { public enum ExitCode { Success = 0, GenerationError, Error, InternalError, UnknownError, } public static partial class Program { [DefaultValue(None)] internal enum TestOptions { None, Regression, QuickConfigure, Configure } #region Log public static bool DebugEnable = false; private static int s_errorCount = 0; private static int s_warningCount = 0; public static void LogWrite(string format, params object[] args) { LogWrite(string.Format(format, args)); } public static void LogWrite(string message) { string prefix = string.Empty; if (DebugEnable) { TimeSpan span = DateTime.Now - Util.ProgramStartTime; prefix = string.Format("[{0:00}:{1:00}] ", span.Minutes, span.Seconds); message = prefix + message; } Console.Write(message); if (Debugger.IsAttached) { message = message.Replace(prefix + Util.CallerInfoTag, string.Empty); Trace.Write(message); } } public static void LogWriteLine(string format, params object[] args) { LogWriteLine(string.Format(format, args)); } public static void LogWriteLine(string msg) { LogWrite(msg + Environment.NewLine); } public static void DebugWrite(string format, params object[] args) { DebugWrite(string.Format(format, args)); } public static void DebugWrite(string msg) { if (DebugEnable) { LogWrite(msg); } } public static void DebugWriteLine(string format, params object[] args) { DebugWriteLine(string.Format(format, args)); } public static void DebugWriteLine(string msg) { if (DebugEnable) DebugWrite(msg + Environment.NewLine); } public static void WarningWrite(string format, params object[] args) { WarningWrite(string.Format(format, args)); } public static void WarningWrite(string msg) { Interlocked.Increment(ref s_warningCount); LogWrite("[WARNING]" + msg); } public static void WarningWriteLine(string format, params object[] args) { WarningWriteLine(string.Format(format, args)); } public static void WarningWriteLine(string msg) { WarningWrite(msg + Environment.NewLine); } public static void ErrorWrite(string format, params object[] args) { ErrorWrite(string.Format(format, args)); } public static void ErrorWrite(string msg) { Interlocked.Increment(ref s_errorCount); LogWrite("[ERROR]" + msg); } public static void ErrorWriteLine(string format, params object[] args) { ErrorWriteLine(string.Format(format, args)); } public static void ErrorWriteLine(string msg) { ErrorWrite(msg + Environment.NewLine); } #endregion private static int Main() { if (CommandLine.ContainParameter("breakintodebugger")) { Console.WriteLine("Debugger requested. Please attach a debugger and press ENTER to continue"); while (Console.ReadKey(true).Key != ConsoleKey.Enter) { Console.WriteLine("Press ENTER to continue"); } Debugger.Break(); } // This GC gives a little bit better results than the other ones. "LowLatency" is giving really bad results(twice slower than the other ones). System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency; Trace.Assert(System.Runtime.GCSettings.IsServerGC, "Server GC is not active! Sharpmake will be much slower!"); AppDomain currentDomain = AppDomain.CurrentDomain; Mutex oneInstanceMutex = null; Argument parameters = new Argument(); ExitCode exitCode = ExitCode.Success; try { DebugEnable = CommandLine.ContainParameter("verbose") || CommandLine.ContainParameter("debug") || CommandLine.ContainParameter("diagnostics"); GetAssemblyInfo(Assembly.GetExecutingAssembly(), out var _, out var version, out var versionString, out var _); LogWriteLine($"sharpmake {versionString}"); LogWriteLine(" arguments: {0}", CommandLine.GetProgramCommandLine()); LogWriteLine(" directory: {0}", Directory.GetCurrentDirectory()); LogWriteLine(" platform: {0} - {1}", Util.GetExecutingPlatform().ToString(), RuntimeInformation.OSDescription); LogWriteLine(" compiled with framework: {0}", Util.FrameworkDisplayName()); LogWriteLine(" running on framework: {0}", RuntimeInformation.FrameworkDescription); LogWriteLine(string.Empty); // display help if wanted and quit if ((CommandLine.GetProgramCommandLine().Length == 0) || CommandLine.ContainParameter("help")) { LogWriteLine(CommandLine.GetCommandLineHelp(typeof(Argument), false)); return CommandLine.ContainParameter("help") ? (int)ExitCode.Success : (int)ExitCode.Error; } AppDomain.CurrentDomain.AssemblyLoad += AppDomain_AssemblyLoad; // Log warnings and errors from builder Assembler.EventOutputError += ErrorWrite; Assembler.EventOutputWarning += WarningWrite; CommandLine.ExecuteOnObject(parameters); if (parameters.Exit) return (int)ExitCode.Success; const string sharpmakeSymbolPrefix = "_SHARPMAKE"; List invalidSymbols = parameters.Defines.Where(define => define.StartsWith(sharpmakeSymbolPrefix)).ToList(); if (invalidSymbols.Any()) { string invalidSymbolsString = string.Join(", ", invalidSymbols); throw new Error($"Only Sharpmake process can define symbols starting with {sharpmakeSymbolPrefix}. Invalid symbols defined: {invalidSymbolsString}"); } parameters.Defines.Add(sharpmakeSymbolPrefix); // A generic sharpmake define to allow scripts to exclude part of code if not used with sharpmake parameters.Defines.Add($"{sharpmakeSymbolPrefix}_{version.Major}_{version.Minor}_X"); parameters.Defines.Add($"{sharpmakeSymbolPrefix}_{version.Major}_{version.Minor}_{version.Build}"); parameters.Validate(); // CommonPlatforms.dll is always loaded by default because those are shipped with // the Sharpmake package. PlatformRegistry.RegisterExtensionAssembly(typeof(Windows.Win32Platform).Assembly); // If any platform declares its own command line options, execute and validate // them as well. IEnumerable platformsCmdLines = PlatformRegistry.GetAvailablePlatforms(); foreach (var platform in platformsCmdLines) { var platformCmdLine = PlatformRegistry.Get(platform); CommandLine.ExecuteOnObject(platformCmdLine); platformCmdLine.Validate(); } bool oneInstanceMutexCreated; string mutexName = string.Format("SharpmakeSingleInstanceMutex{0}", parameters.MutexSuffix); // Allow custom mutex name suffix. Useful to debug concurrently multiple sharpmake running from different branches oneInstanceMutex = new Mutex(true, mutexName, out oneInstanceMutexCreated); if (!oneInstanceMutexCreated) { try { if (!oneInstanceMutex.WaitOne(0)) { LogWriteLine("wait for another instance(s) of sharpmake to terminate..."); oneInstanceMutex.WaitOne(); } } catch (AbandonedMutexException) { // This occurs if another sharpmake is killed in the debugger } finally { LogWriteLine("waiting done."); } } if (parameters.RegexMatchCacheEnabled) { GlobalRegexMatchCache.Init(parameters.RegexMatchCacheInitialCapacity); } switch (parameters.TestOption) { case TestOptions.Regression: { var regressionTest = new BuildContext.RegressionTest(parameters.OutputDirectory, parameters.ReferenceDirectory, parameters.RemapRoot); GenerateAll(regressionTest, parameters); exitCode = ExitCode.Success; var regressions = regressionTest.GetRegressions().ToList(); if (regressions.Count > 0) { exitCode = ExitCode.Error; DebugWriteLine($"{regressions.Count} Regressions detected:"); List fileChanges = regressions.Where(x => x.FileStatus == BuildContext.RegressionTest.FileStatus.Different).ToList(); LogFileChanges(fileChanges, parameters.RegressionDiff); var fileMissing = regressions.Where(x => x.FileStatus == BuildContext.RegressionTest.FileStatus.NotGenerated).Select(x => x.ReferencePath).ToList(); if (fileMissing.Count > 0) { fileMissing.Sort(); DebugWriteLine($" {fileMissing.Count} files are missing from the output:"); fileMissing.ForEach(x => DebugWriteLine($" {x}")); } } } break; case TestOptions.QuickConfigure: { exitCode = AnalyzeConfigureOrder(parameters, true); } break; case TestOptions.Configure: { exitCode = AnalyzeConfigureOrder(parameters, false); } break; case TestOptions.None: default: { if (parameters.OutputDirectory != null) { // output redirect mode var redirectOutput = new BuildContext.RedirectOutput(parameters.OutputDirectory, parameters.RemapRoot); GenerateAll(redirectOutput, parameters); exitCode = ExitCode.Success; } else { var generateAll = new BuildContext.GenerateAll(parameters.DebugLog, parameters.WriteFiles); GenerateAll(generateAll, parameters); exitCode = ExitCode.Success; Util.ExecuteFilesAutoCleanup(); } } break; } if (CSproj.AllCsProjSubTypesInfos.Any()) Util.SerializeAllCsprojSubTypes(CSproj.AllCsProjSubTypesInfos); if (parameters.RegexMatchCacheEnabled) { int regexMatchCacheInitialCapacity = parameters.RegexMatchCacheInitialCapacity; int regexMatchCacheSize = GlobalRegexMatchCache.Count; if (regexMatchCacheInitialCapacity < regexMatchCacheSize) { WarningWriteLine("Warning (perf): Consider increasing regex match cache initial capacity from {0} to at least {1} ( /regexMatchCacheInitialCapacity({1}) ).", regexMatchCacheInitialCapacity, regexMatchCacheSize); } GlobalRegexMatchCache.UnInit(); } } catch (Error e) { // Log error message Exception innerException = e; while (innerException.InnerException != null) innerException = innerException.InnerException; ErrorWriteLine(Environment.NewLine + "Error:" + Environment.NewLine + innerException.Message); // Then log details LogWriteLine(Util.GetCompleteExceptionMessage(e, "\t")); exitCode = ExitCode.Error; } catch (InternalError e) { ErrorWriteLine(Environment.NewLine + "Internal Error:"); LogWriteLine(Util.GetCompleteExceptionMessage(e, "\t")); exitCode = ExitCode.InternalError; } catch (Exception e) { LogWriteLine(Environment.NewLine + "Exception Error:"); LogWriteLine(Util.GetCompleteExceptionMessage(e, "\t")); exitCode = ExitCode.UnknownError; } finally { if (oneInstanceMutex != null) { oneInstanceMutex.ReleaseMutex(); GC.KeepAlive(oneInstanceMutex); } if (parameters.Debug) { Console.WriteLine("DEBUG Sharpmake.Application: Press any key to exit..."); Console.ReadKey(); } } if (exitCode <= ExitCode.Error) // Do not display summary in case of unknown exception or internal error LogWriteLine(@"{0} errors, {1} warnings", s_errorCount, s_warningCount); if (s_errorCount != 0) { if (Debugger.IsAttached) { LogWriteLine("Please look at the errors."); Debugger.Break(); } } // Always return the same error code no matter the number of errors. if (exitCode == ExitCode.Success && s_errorCount != 0) { exitCode = ExitCode.GenerationError; } return (int)exitCode; } private static void AppDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args) { LogSharpmakeExtensionLoaded(args.LoadedAssembly); } /// /// Retrieve information from the assembly passed as argument /// /// The assembly to get information from /// The name of the assembly /// The version as read from the assembly (the 4th componant is not used by Sharpmake core, but may be used by Sharpmake extended assemblies) /// A string of the format " [(non-official)] [()]" /// The location of the assembly private static void GetAssemblyInfo(Assembly assembly, out string name, out Version version, out string versionString, out string location) { // Name name = assembly.GetName().Name; // Version version = assembly.GetName().Version; versionString = "(non-official)"; string informationalVersion = assembly.GetCustomAttribute()?.InformationalVersion; if (!string.IsNullOrEmpty(informationalVersion)) { var endOfVersionIndex = informationalVersion.IndexOf("+", StringComparison.Ordinal); if (endOfVersionIndex != -1) { versionString = informationalVersion[..endOfVersionIndex]; if (informationalVersion.IndexOf("-g", 0, endOfVersionIndex, StringComparison.Ordinal) != -1 && informationalVersion.IndexOf(".Commits.0.", 0, endOfVersionIndex, StringComparison.Ordinal) == -1) { versionString += " (non-official)"; } } if (DebugEnable) versionString += $" ({informationalVersion})"; } // Location location = assembly.Location; } private static void LogSharpmakeExtensionLoaded(Assembly extensionAssembly) { if (extensionAssembly == null) return; if (!ExtensionLoader.ExtensionChecker.IsSharpmakeExtension(extensionAssembly)) return; GetAssemblyInfo(extensionAssembly, out var extensionName, out var _, out var extensionVersion, out var extensionLocation); LogWriteLine(" {0} {1} loaded from '{2}' in assembly load context '{3}'", extensionName, extensionVersion, extensionLocation, AssemblyLoadContext.GetLoadContext(extensionAssembly).Name); } private static void CreateBuilderAndGenerate(BuildContext.BaseBuildContext buildContext, Argument parameters, bool generateDebugSolution) { string cleanupSuffixOldValue = Util.FilesAutoCleanupDBSuffix; bool cleanupActiveOldValue = Util.FilesAutoCleanupActive; if (generateDebugSolution) { // Set a cleanup context exclusive to debug solution Util.FilesAutoCleanupDBSuffix = "_debugsolution"; Util.FilesAutoCleanupActive = true; if (!string.IsNullOrEmpty(parameters.DebugSolutionPath)) Util.FilesAutoCleanupDBPath = parameters.DebugSolutionPath; else Util.FilesAutoCleanupDBPath = Path.GetDirectoryName(parameters.Sources[0]); } using (Builder builder = CreateBuilder(buildContext, parameters, allowCleanBlobs: true, generateDebugSolution: generateDebugSolution)) { if (parameters.CleanBlobsOnly) { LogWriteLine("success:"); LogWriteLine(" blobs {0,4} cleaned, {1,3} already cleaned", Project.BlobCleaned, Project.BlobAlreadyCleaned); } else if (parameters.BlobOnly) { LogWriteLine("success:"); LogWriteLine(" blobs {0,4} generated, {1,3} up-to-date", Project.BlobGenerated, Project.BlobUpdateToDate); } else { IDictionary outputs; using (Builder.Instance.CreateProfilingScope("Generate")) outputs = builder.Generate(); foreach (var output in outputs) { if (output.Value.Exception != null) throw new Error(output.Value.Exception, "Error encountered while generating {0}", output.Key); } if (!string.IsNullOrEmpty(parameters.ProfileFile)) builder.DumpTraceFile(parameters.ProfileFile); if (parameters.DumpDependency) DependencyTracker.Instance.DumpGraphs(outputs); LogWriteGenerateResults(outputs); } } LogWriteLine(" time: {0:0.00} sec.", (DateTime.Now - Util.ProgramStartTime).TotalSeconds); LogWriteLine(" completed on {0}.", DateTime.Now); if (generateDebugSolution) { // Execute cleanup for debug solution generation Util.ExecuteFilesAutoCleanup(true); // Restore original cleanup context Util.FilesAutoCleanupDBSuffix = cleanupSuffixOldValue; Util.FilesAutoCleanupActive = cleanupActiveOldValue; } } private static void GenerateAll(BuildContext.BaseBuildContext buildContext, Argument parameters) { if (parameters.GenerateDebugSolution) { CreateBuilderAndGenerate(buildContext, parameters, generateDebugSolution: true); // because the debug solution generation runs before the user code, // we need to do some cleanup so we don't pollute the subsequent generation ExtensionMethods.ClearVisualStudioDirCaches(); } if (!parameters.GenerateDebugSolutionOnly) { CreateBuilderAndGenerate(buildContext, parameters, generateDebugSolution: false); } } private static ExitCode AnalyzeConfigureOrder(Argument parameters, bool stopOnFirstError) { // Analyze .sharpmake code var report = Analyzer.Analyzer.AnalyzeConfigure(context => CreateBuilder(context, parameters, false), stopOnFirstError); Console.WriteLine("{0} faulty root methods.", report.Count()); using (var stream = File.Open("faultymethods.txt", FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) { if (report.Any()) { Console.WriteLine("Faulty methods are dependent on a specific Configure() call order. See faultymethods.txt"); using (var writer = new StreamWriter(stream)) { writer.WriteLine("You can use www.yuml.me to view this in a diagram."); var set = new SortedSet(); foreach (var root in report.OrderBy(x => x.Method.Name)) { RecursivePrintMethodInfo(root, set); } foreach (var line in set) { writer.WriteLine(line); } } return ExitCode.Error; } } return ExitCode.Success; } public static void LogWriteGenerateResults(IDictionary outputs) { var projects = outputs.Where(o => typeof(Project).IsAssignableFrom(o.Key)); var solutions = outputs.Where(o => typeof(Solution).IsAssignableFrom(o.Key)); var others = outputs.Where(o => ((!typeof(Project).IsAssignableFrom(o.Key)) && (!typeof(Solution).IsAssignableFrom(o.Key)))); var generatedProjectFiles = new List(projects.SelectMany(o => o.Value.Generated)); var skippedProjectFiles = new List(projects.SelectMany(o => o.Value.Skipped)); var generatedSolutionFiles = new List(solutions.SelectMany(o => o.Value.Generated)); var skippedSolutionFiles = new List(solutions.SelectMany(o => o.Value.Skipped)); var generatedOtherFiles = new List(others.SelectMany(o => o.Value.Generated)); var skippedOtherFiles = new List(others.SelectMany(o => o.Value.Skipped)); if (generatedProjectFiles.Count != 0) { LogWriteLine(" " + generatedProjectFiles.Count + " project" + (generatedProjectFiles.Count > 1 ? "s" : "") + " generated:"); generatedProjectFiles.Sort(); foreach (string file in generatedProjectFiles) LogWriteLine(" {0}", file); } if (generatedSolutionFiles.Count != 0) { LogWriteLine(" " + generatedSolutionFiles.Count + " solution" + (generatedSolutionFiles.Count > 1 ? "s" : "") + " generated:"); generatedSolutionFiles.Sort(); foreach (string file in generatedSolutionFiles) LogWriteLine(" {0}", file); } if (Project.FastBuildMasterGeneratedFiles.Count > 0) { LogWriteLine(" " + Project.FastBuildMasterGeneratedFiles.Count + " fastbuild master bff generated:"); Project.FastBuildMasterGeneratedFiles.Sort(); foreach (string file in Project.FastBuildMasterGeneratedFiles) LogWriteLine(" {0}", file); } if (generatedOtherFiles.Count != 0) { LogWriteLine(" " + generatedOtherFiles.Count + " other file" + (generatedOtherFiles.Count > 1 ? "s" : "") + " generated:"); generatedOtherFiles.Sort(); foreach (string file in generatedOtherFiles) LogWriteLine(" {0}", file); } LogWriteLine(" Results:"); if (generatedProjectFiles.Count > 0 || skippedProjectFiles.Count > 0) { LogWriteLine(" projects ({0,5} configurations) {1,5} generated, {2,5} up-to-date", Project.Configuration.Count, generatedProjectFiles.Count, skippedProjectFiles.Count); if (Project.FastBuildGeneratedFileCount > 0 || Project.FastBuildUpToDateFileCount > 0) LogWriteLine(" fastbuild {0,5} generated, {1,5} up-to-date", Project.FastBuildGeneratedFileCount, Project.FastBuildUpToDateFileCount); } if (generatedSolutionFiles.Count > 0 || skippedSolutionFiles.Count > 0) LogWriteLine(" solutions ({0,5} configurations) {1,5} generated, {2,5} up-to-date", Solution.Configuration.Count, generatedSolutionFiles.Count, skippedSolutionFiles.Count); if (Project.BlobGenerated > 0 || Project.BlobUpdateToDate > 0) LogWriteLine(" blobs {0,5} generated, {1,5} up-to-date", Project.BlobGenerated, Project.BlobUpdateToDate); if (generatedOtherFiles.Count > 0 || skippedOtherFiles.Count > 0) LogWriteLine(" other files {0,5} generated, {1,5} up-to-date", generatedOtherFiles.Count, skippedOtherFiles.Count); } public static Builder CreateBuilder(BuildContext.BaseBuildContext context, Argument parameters, bool allowCleanBlobs, bool generateDebugSolution = false) { Builder builder = new Builder( context, parameters.Multithreaded, parameters.DumpDependency, allowCleanBlobs && parameters.CleanBlobsOnly, parameters.BlobOnly, parameters.SkipInvalidPath, parameters.Diagnostics, debugScripts: true, // warning: some code that rely on callstacks misbehaves in release, because methods can completely disappear due to optimizations, so force disable for now getGeneratorsManagerCallBack: GetGeneratorsManager, defines: parameters.Defines ); // Allow message log from builder. builder.EventOutputError += ErrorWrite; builder.EventOutputWarning += WarningWrite; builder.EventOutputMessage += LogWrite; builder.EventOutputDebug += DebugWrite; if (!string.IsNullOrEmpty(parameters.ProfileFile)) builder.EnableProfiling(); try { // Generate debug solution using (Builder.Instance.CreateProfilingScope("Debug solution")) { if (generateDebugSolution) { LogWriteLine("Generate debug solution..."); DebugProjectGenerator.GenerateDebugSolution(parameters.Sources, parameters.DebugSolutionPath, builder.Arguments, parameters.DebugSolutionStartArguments, parameters.DebugSolutionDevEnv); builder.BuildProjectAndSolution(); return builder; } } // Load user input (either files or pre-built assemblies) using (Builder.Instance.CreateProfilingScope("EntryPoints")) { switch (parameters.Input) { case Argument.InputType.File: builder.ExecuteEntryPointInAssemblies
(builder.LoadSharpmakeFiles(parameters.Sources)); break; case Argument.InputType.Assembly: builder.ExecuteEntryPointInAssemblies
(builder.LoadAssemblies(parameters.Assemblies)); break; case Argument.InputType.Undefined: default: throw new Error("Sharpmake input missing, use /sources() or /assemblies()"); } } if (builder.Arguments.TypesToGenerate.Count == 0) throw new Error("Sharpmake has nothing to generate!" + Environment.NewLine + $" Make sure to have a static entry point method flagged with [{typeof(Main).FullName}] attribute, and add 'arguments.Generate<[your_class]>();' in it."); builder.Context.ConfigureOrder = builder.Arguments.ConfigureOrder; // Call all configuration's methods and resolve project/solution member's values using (Builder.Instance.CreateProfilingScope("Build")) builder.BuildProjectAndSolution(); return builder; } catch { builder.Dispose(); throw; } } private static void RecursivePrintMethodInfo(Analyzer.ConfigureMethodInfo method, ISet set, int nested = 0) { ++nested; foreach (var dependent in method.Dependents.OrderBy(x => x.Method.ToString())) { set.Add(string.Format("[{0}]->[{1}]", dependent, method)); if (dependent.Dependents.Any()) RecursivePrintMethodInfo(dependent, set, nested); } } private static string LocateDiffExecutable() { var candidateDirectories = new List(); if (!Util.UsesUnixSeparator) // poor way to test the OS... { try { candidateDirectories.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Git\usr\bin")); candidateDirectories.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Git\usr\bin")); } catch { } } candidateDirectories.AddRange(Environment.GetEnvironmentVariable("PATH").Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)); foreach (var candidateDirectory in candidateDirectories) { foreach (var candidateExeName in new[] { "diff", "diff.exe" }) { var candidatePath = Path.Combine(candidateDirectory, candidateExeName); if (File.Exists(candidatePath)) return candidatePath; } } return null; } private static void LogFileChanges(List fileChanges, bool showRegressionDiff) { if (fileChanges.Count == 0) return; var diffs = new Dictionary>(StringComparer.OrdinalIgnoreCase); object dictionaryAccess = new object(); string diffExecutable = LocateDiffExecutable(); if (!showRegressionDiff || diffExecutable == null) { DebugWriteLine($" {fileChanges.Count} files have changed from the reference:"); fileChanges.ForEach(x => { DebugWriteLine($" Exp: {x.ReferencePath}"); DebugWriteLine($" Was: {x.OutputPath}"); }); } else { DebugWriteLine($" {fileChanges.Count} files have changed from the reference. Aggregating diff using '{diffExecutable}'"); Parallel.ForEach(fileChanges, x => { bool refFileExists = File.Exists(x.ReferencePath); bool outFileExists = File.Exists(x.OutputPath); if (refFileExists && outFileExists) { Process process = new Process(); process.StartInfo.FileName = diffExecutable; process.StartInfo.UseShellExecute = false; process.StartInfo.CreateNoWindow = true; process.StartInfo.RedirectStandardOutput = true; // -i, --ignore-case ignore case differences in file contents // -u, -U NUM, --unified[=NUM] output NUM (default 3) lines of unified context // -w, --ignore-all-space ignore all white space process.StartInfo.Arguments = $"-i -u -w {x.ReferencePath} {x.OutputPath}"; process.Start(); var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); var lines = output.Split('\n').Where(l => (l.Length > 1 && (l[0] == '+' || l[0] == '-') && !l.StartsWith("--- ") && !l.StartsWith("+++ "))); var diff = string.Concat(lines); lock (dictionaryAccess) { List currentList = null; if (!diffs.TryGetValue(diff, out currentList)) diffs[diff] = new List { x.OutputPath }; else currentList.Add(x.OutputPath); } } else if (!refFileExists) DebugWriteLine($" ExtraFileGenerated: {x.OutputPath}"); else if (!outFileExists) DebugWriteLine($" MissingFileInOutput: {x.ReferencePath}"); }); int i = 0; foreach (var diff in diffs.OrderByDescending(d => d.Value.Count)) { DebugWriteLine( $" Diff block {++i}/{diffs.Count}" + (diff.Value.Count > 1 ? $" shared by {diff.Value.Count} files:" : " only in '" + diff.Value.First() + "':") ); int j = 0; foreach (var file in diff.Value.OrderBy(f => f)) DebugWriteLine($" {++j}/{diff.Value.Count} {file}"); var diffLines = diff.Key.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); if (diffLines.Length == 0) { DebugWriteLine(" // only whitespace or casing changes"); } else { foreach (var diffLine in diffLines) DebugWriteLine($" {diffLine}"); } } } } public static IGeneratorManager GetGeneratorsManager() { return new GeneratorManager(); } } }