File: Program.cs
Web Access
Project: ..\..\..\src\Compatibility\ApiDiff\Microsoft.DotNet.ApiDiff.Tool\Microsoft.DotNet.ApiDiff.Tool.csproj (Microsoft.DotNet.ApiDiff.Tool)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.CommandLine;
using System.ComponentModel;
using System.Diagnostics;
using Microsoft.DotNet.ApiSymbolExtensions.Logging;
 
namespace Microsoft.DotNet.ApiDiff.Tool;
 
/// <summary>
/// Entrypoint for the genapidiff tool, which generates a markdown diff of two
/// different versions of the same assembly, using the specified command line options.
/// </summary>
public static class Program
{
    private static readonly string AttributesToExcludeDefaultFileName = "AttributesToExclude.txt";
 
    public static async Task Main(string[] args)
    {
        RootCommand rootCommand = new("ApiDiff - Tool for generating a markdown diff of two different versions of the same assembly.");
 
        Option<string> optionBeforeAssembliesFolderPath = new("--before", "-b")
        {
            Description = "The path to the folder containing the old (before) assemblies to be included in the diff.",
            Arity = ArgumentArity.ExactlyOne,
            Required = true
        };
 
        Option<string> optionBeforeRefAssembliesFolderPath = new("--refbefore", "-rb")
        {
            Description = "The path to the folder containing the references required by old (before) assemblies, not to be included in the diff.",
            Arity = ArgumentArity.ExactlyOne,
            Required = false
        };
 
        Option<string> optionAfterAssembliesFolderPath = new("--after", "-a")
        {
            Description = "The path to the folder containing the new (after) assemblies to be included in the diff.",
            Arity = ArgumentArity.ExactlyOne,
            Required = true
        };
 
        Option<string> optionAfterRefAssembliesFolderPath = new("--refafter", "-ra")
        {
            Description = "The path to the folder containing references required by the new (after) reference assemblies, not to be included in the diff.",
            Arity = ArgumentArity.ExactlyOne,
            Required = false
        };
 
        Option<string> optionOutputFolderPath = new("--output", "-o")
        {
            Description = "The path to the output folder.",
            Arity = ArgumentArity.ExactlyOne,
            Required = true
        };
 
        Option<string> optionBeforeFriendlyName = new("--beforeFriendlyName", "-bfn")
        {
            Description = "The friendly name to describe the 'before' assembly.",
            Arity = ArgumentArity.ExactlyOne,
            Required = true
        };
 
        Option<string> optionAfterFriendlyName = new("--afterFriendlyName", "-afn")
        {
            Description = "The friendly name to describe the 'after' assembly.",
            Arity = ArgumentArity.ExactlyOne,
            Required = true
        };
 
        Option<string> optionTableOfContentsTitle = new("--tableOfContentsTitle", "-tc")
        {
            Description = $"The optional title of the markdown table of contents file that is placed in the output folder.",
            Arity = ArgumentArity.ExactlyOne,
            Required = false,
            DefaultValueFactory = _ => "api_diff"
        };
 
        Option<FileInfo[]?> optionFilesWithAssembliesToExclude = new("--assembliesToExclude", "-eas")
        {
            Description = "An optional array of filepaths, each containing a list of assemblies that should be excluded from the diff. Each file should contain one assembly name per line, with no extensions.",
            Arity = ArgumentArity.ZeroOrMore,
            Required = false,
            DefaultValueFactory = _ => null
        };
 
        Option<FileInfo[]?> optionFilesWithAttributesToExclude = new("--attributesToExclude", "-eattrs")
        {
            Description = $"An optional array of filepaths, each containing a list of attributes to exclude from the diff. Each file should contain one API full name per line. You can either modify the default file '{AttributesToExcludeDefaultFileName}' to add your own attributes, or include additional files using this command line option.",
            Arity = ArgumentArity.ZeroOrMore,
            Required = false,
            DefaultValueFactory = _ => [new FileInfo(AttributesToExcludeDefaultFileName)]
        };
 
        Option<FileInfo[]?> optionFilesWithApisToExclude = new("--apisToExclude", "-eapis")
        {
            Description = "An optional array of filepaths, each containing a list of APIs to exclude from the diff. Each file should contain one API full name per line.",
            Arity = ArgumentArity.ZeroOrMore,
            Required = false,
            DefaultValueFactory = _ => null
        };
 
        Option<bool> optionAddPartialModifier = new("--addPartialModifier", "-apm")
        {
            Description = "Add the 'partial' modifier to types.",
            DefaultValueFactory = _ => false
        };
 
        Option<bool> optionAttachDebugger = new("--attachDebugger", "-d")
        {
            Description = "Stops the tool at startup, prints the process ID and waits for a debugger to attach.",
            DefaultValueFactory = _ => false
        };
 
        // Custom ordering for the help menu.
        rootCommand.Options.Add(optionBeforeAssembliesFolderPath);
        rootCommand.Options.Add(optionBeforeRefAssembliesFolderPath);
        rootCommand.Options.Add(optionAfterAssembliesFolderPath);
        rootCommand.Options.Add(optionAfterRefAssembliesFolderPath);
        rootCommand.Options.Add(optionOutputFolderPath);
        rootCommand.Options.Add(optionBeforeFriendlyName);
        rootCommand.Options.Add(optionAfterFriendlyName);
        rootCommand.Options.Add(optionTableOfContentsTitle);
        rootCommand.Options.Add(optionFilesWithAssembliesToExclude);
        rootCommand.Options.Add(optionFilesWithAttributesToExclude);
        rootCommand.Options.Add(optionFilesWithApisToExclude);
        rootCommand.Options.Add(optionAddPartialModifier);
        rootCommand.Options.Add(optionAttachDebugger);
 
        rootCommand.SetAction(async (ParseResult result, CancellationToken cancellationToken) =>
        {
            DiffConfiguration diffConfig = new(
                BeforeAssembliesFolderPath: result.GetValue(optionBeforeAssembliesFolderPath) ?? throw new NullReferenceException("Null before assemblies directory"),
                BeforeAssemblyReferencesFolderPath: result.GetValue(optionBeforeRefAssembliesFolderPath),
                AfterAssembliesFolderPath: result.GetValue(optionAfterAssembliesFolderPath) ?? throw new NullReferenceException("Null after assemblies directory"),
                AfterAssemblyReferencesFolderPath: result.GetValue(optionAfterRefAssembliesFolderPath),
                OutputFolderPath: result.GetValue(optionOutputFolderPath) ?? throw new NullReferenceException("Null output directory"),
                BeforeFriendlyName: result.GetValue(optionBeforeFriendlyName) ?? throw new NullReferenceException("Null before friendly name"),
                AfterFriendlyName: result.GetValue(optionAfterFriendlyName) ?? throw new NullReferenceException("Null after friendly name"),
                TableOfContentsTitle: result.GetValue(optionTableOfContentsTitle) ?? throw new NullReferenceException("Null table of contents title"),
                FilesWithAssembliesToExclude: result.GetValue(optionFilesWithAssembliesToExclude),
                FilesWithAttributesToExclude: result.GetValue(optionFilesWithAttributesToExclude),
                FilesWithApisToExclude: result.GetValue(optionFilesWithApisToExclude),
                AddPartialModifier: result.GetValue(optionAddPartialModifier),
                AttachDebugger: result.GetValue(optionAttachDebugger)
            );
            await HandleCommandAsync(diffConfig, cancellationToken).ConfigureAwait(false);
        });
        await rootCommand.Parse(args).InvokeAsync();
    }
 
    private static Task HandleCommandAsync(DiffConfiguration diffConfig, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        var log = new ConsoleLog(MessageImportance.Normal);
 
        string assembliesToExclude = string.Join(", ", diffConfig.FilesWithAssembliesToExclude?.Select(a => a.FullName) ?? []);
        string attributesToExclude = string.Join(", ", diffConfig.FilesWithAttributesToExclude?.Select(a => a.FullName) ?? []);
        string apisToExclude = string.Join(", ", diffConfig.FilesWithApisToExclude?.Select(a => a.FullName) ?? []);
 
        // Custom ordering to match help menu.
        log.LogMessage("Selected options:");
        log.LogMessage($" - 'Before' source assemblies:         {diffConfig.BeforeAssembliesFolderPath}");
        log.LogMessage($" - 'After'  source assemblies:         {diffConfig.AfterAssembliesFolderPath}");
        log.LogMessage($" - 'Before' reference assemblies:      {diffConfig.BeforeAssemblyReferencesFolderPath}");
        log.LogMessage($" - 'After'  reference assemblies:      {diffConfig.AfterAssemblyReferencesFolderPath}");
        log.LogMessage($" - Output:                             {diffConfig.OutputFolderPath}");
        log.LogMessage($" - Files with assemblies to exclude:   {assembliesToExclude}");
        log.LogMessage($" - Files with attributes to exclude:   {attributesToExclude}");
        log.LogMessage($" - Files with APIs to exclude:         {apisToExclude}");
        log.LogMessage($" - 'Before' friendly name:             {diffConfig.BeforeFriendlyName}");
        log.LogMessage($" - 'After' friendly name:              {diffConfig.AfterFriendlyName}");
        log.LogMessage($" - Table of contents title:            {diffConfig.TableOfContentsTitle}");
        log.LogMessage($" - Add partial modifier to types:      {diffConfig.AddPartialModifier}");
        log.LogMessage($" - Attach debugger:                    {diffConfig.AttachDebugger}");
        log.LogMessage("");
 
        if (diffConfig.AttachDebugger)
        {
            WaitForDebugger();
        }
 
        IDiffGenerator diffGenerator = DiffGeneratorFactory.Create(log,
                                                                   diffConfig.BeforeAssembliesFolderPath,
                                                                   diffConfig.BeforeAssemblyReferencesFolderPath,
                                                                   diffConfig.AfterAssembliesFolderPath,
                                                                   diffConfig.AfterAssemblyReferencesFolderPath,
                                                                   diffConfig.OutputFolderPath,
                                                                   diffConfig.BeforeFriendlyName,
                                                                   diffConfig.AfterFriendlyName,
                                                                   diffConfig.TableOfContentsTitle,
                                                                   diffConfig.FilesWithAssembliesToExclude,
                                                                   diffConfig.FilesWithAttributesToExclude,
                                                                   diffConfig.FilesWithApisToExclude,
                                                                   diffConfig.AddPartialModifier,
                                                                   writeToDisk: true,
                                                                   diagnosticOptions: null // TODO: If needed, add CLI option to pass specific diagnostic options
                                                                   );
 
        return diffGenerator.RunAsync(cancellationToken);
    }
 
    private static void WaitForDebugger()
    {
        while (!Debugger.IsAttached)
        {
            Console.WriteLine($"Attach to process {Environment.ProcessId}...");
            Thread.Sleep(1000);
        }
        Console.WriteLine("Debugger attached!");
        Debugger.Break();
    }
}