File: MSBuildArgs.cs
Web Access
Project: ..\..\..\src\Cli\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj (Microsoft.DotNet.Cli.Utils)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.ObjectModel;
using System.CommandLine;
using System.CommandLine.Parsing;
 
namespace Microsoft.DotNet.Cli.Utils;
 
/// <summary>
/// Represents all of the parsed and forwarded arguments that the SDK should pass to MSBuild.
/// </summary>
public sealed class MSBuildArgs
{
    private MSBuildArgs(
        ReadOnlyDictionary<string, string>? properties,
        ReadOnlyDictionary<string, string>? restoreProperties,
        string[]? targets,
        string[]? getProperty,
        string[]? getItem,
        string[]? getTargetResult,
        string[]? getResultOutputFile,
        VerbosityOptions? verbosity,
        string[]? otherMSBuildArgs)
    {
        GlobalProperties = properties;
        RestoreGlobalProperties = restoreProperties;
        RequestedTargets = targets;
        GetProperty = getProperty;
        GetItem = getItem;
        GetTargetResult = getTargetResult;
        GetResultOutputFile = getResultOutputFile;
        Verbosity = verbosity;
        OtherMSBuildArgs = otherMSBuildArgs is not null
            ? [.. otherMSBuildArgs]
            : new List<string>();
    }
 
    /// <summary>
    /// The set of <c>-p</c> flags that should be passed to MSBuild.
    /// </summary>
    public ReadOnlyDictionary<string, string>? GlobalProperties { get; }
    /// <summary>
    /// The set of <c>-rp</c> flags that should be passed to MSBuild for restore operations only.
    /// If this is non-empty, all <see cref="GlobalProperties"/> flags should be passed as <c>-rp</c> as well.
    /// </summary>
    public ReadOnlyDictionary<string, string>? RestoreGlobalProperties { get; private set; }
 
    /// <summary>
    /// The ordered list of targets that should be passed to MSBuild.
    /// </summary>
    public string[]? RequestedTargets { get; }
 
    public string[]? GetProperty { get; }
 
    public string[]? GetItem { get; }
 
    public string[]? GetTargetResult { get; }
 
    public string[]? GetResultOutputFile { get; }
 
    public VerbosityOptions? Verbosity { get; }
 
    /// <summary>
    /// All other arguments that aren't already explicitly modeled by this structure.
    /// The main categories of these today are logger configurations
    /// </summary>
    public List<string> OtherMSBuildArgs { get; }
 
    /// <summary>
    /// Ensures that when we do our MSBuild-property re-parses we parse in the same way as the dotnet CLI's parser.
    /// </summary>
    private static readonly ParserConfiguration _analysisParsingConfiguration = new()
    {
        EnablePosixBundling = false
    };
 
    /// <summary>
    /// Takes all of the unstructured properties and arguments that have been accrued from the command line
    /// processing of the SDK and returns a structured set of MSBuild arguments grouped by purpose.
    /// </summary>
    /// <param name="forwardedAndUserFacingArgs">the complete set of forwarded MSBuild arguments and un-parsed, potentially MSBuild-relevant arguments</param>
    public static MSBuildArgs AnalyzeMSBuildArguments(IEnumerable<string> forwardedAndUserFacingArgs, params Option[] options)
    {
        var fakeCommand = new System.CommandLine.Command("dotnet");
        foreach (var option in options)
        {
            fakeCommand.Options.Add(option);
        }
 
        var parseResult = fakeCommand.Parse([.. forwardedAndUserFacingArgs], _analysisParsingConfiguration);
        var globalProperties = parseResult.GetResult("--property") is OptionResult propResult ? propResult.GetValueOrDefault<ReadOnlyDictionary<string, string>?>() : null;
        var restoreProperties = parseResult.GetResult("--restoreProperty") is OptionResult restoreResult ? restoreResult.GetValueOrDefault<ReadOnlyDictionary<string, string>?>() : null;
        var requestedTargets = parseResult.GetResult("--target") is OptionResult targetResult ? targetResult.GetValueOrDefault<string[]?>() : null;
        var getProperty = TryGetValue<string[]>("--getProperty");
        var getItem = TryGetValue<string[]?>("--getItem");
        var getTargetResult = TryGetValue<string[]?>("--getTargetResult");
        var getResultOutputFile = TryGetValue<string[]?>("--getResultOutputFile");
        var verbosity = parseResult.GetResult("--verbosity") is OptionResult verbosityResult
            ? verbosityResult.GetValueOrDefault<VerbosityOptions?>()
            : null;
        var otherMSBuildArgs = parseResult.UnmatchedTokens.ToArray();
        return new MSBuildArgs(
            properties: globalProperties,
            restoreProperties: restoreProperties,
            targets: requestedTargets,
            getProperty: getProperty,
            getItem: getItem,
            getTargetResult: getTargetResult,
            getResultOutputFile: getResultOutputFile,
            otherMSBuildArgs: otherMSBuildArgs,
            verbosity: verbosity);
 
        T? TryGetValue<T>(string name)
        {
            return options.Any(o => o.Name == name) ? parseResult.GetValue<T>(name) : default;
        }
    }
 
 
    public static MSBuildArgs FromProperties(ReadOnlyDictionary<string, string>? properties)
    {
        return new MSBuildArgs(properties, null, null, null, null, null, null, null, null);
    }
 
    public static MSBuildArgs FromOtherArgs(params ReadOnlySpan<string> args)
    {
        return new MSBuildArgs(null, null, null, null, null, null, null, null, args.ToArray());
    }
    public static MSBuildArgs FromVerbosity(VerbosityOptions verbosity)
    {
        return new MSBuildArgs(null, null, null, null, null, null, null, verbosity, null);
    }
 
    public static readonly MSBuildArgs ForHelp = new(null, null, null, null, null, null, null, null, ["--help"]);
 
    /// <summary>
    /// Completely replaces the MSBuild arguments with the provided <paramref name="newArgs"/>.
    /// </summary>
    public MSBuildArgs CloneWithExplicitArgs(string[] newArgs)
    {
        return new MSBuildArgs(
            properties: GlobalProperties,
            restoreProperties: RestoreGlobalProperties,
            targets: RequestedTargets,
            getProperty: GetProperty,
            getItem: GetItem,
            getTargetResult: GetTargetResult,
            getResultOutputFile: GetResultOutputFile,
            otherMSBuildArgs: newArgs,
            verbosity: Verbosity);
    }
 
    /// <summary>
    /// Adds new arbitrary MSBuild flags to the end of the existing MSBuild arguments.
    /// </summary>
    public MSBuildArgs CloneWithAdditionalArgs(params string[] additionalArgs)
    {
        if (additionalArgs is null || additionalArgs.Length == 0)
        {
            // If there are no additional args, we can just return the current instance.
            return new MSBuildArgs(
                GlobalProperties,
                RestoreGlobalProperties,
                RequestedTargets,
                GetProperty,
                GetItem,
                GetTargetResult,
                GetResultOutputFile,
                Verbosity,
                OtherMSBuildArgs.ToArray());
        }
 
        return new MSBuildArgs(
            GlobalProperties,
            RestoreGlobalProperties,
            RequestedTargets,
            GetProperty,
            GetItem,
            GetTargetResult,
            GetResultOutputFile,
            Verbosity,
            [.. OtherMSBuildArgs, .. additionalArgs]);
    }
 
    public MSBuildArgs CloneWithAdditionalRestoreProperties(ReadOnlyDictionary<string, string>? additionalRestoreProperties)
    {
        if (additionalRestoreProperties is null || additionalRestoreProperties.Count == 0)
        {
            // If there are no additional restore properties, we can just return the current instance.
            return new MSBuildArgs(
                GlobalProperties,
                RestoreGlobalProperties,
                RequestedTargets,
                GetProperty,
                GetItem,
                GetTargetResult,
                GetResultOutputFile,
                Verbosity,
                OtherMSBuildArgs.ToArray());
        }
        if (RestoreGlobalProperties is null)
        {
            return new MSBuildArgs(
                GlobalProperties,
                additionalRestoreProperties,
                RequestedTargets,
                GetProperty,
                GetItem,
                GetTargetResult,
                GetResultOutputFile,
                Verbosity,
                OtherMSBuildArgs.ToArray());
        }
 
        var newRestoreProperties = new Dictionary<string, string>(RestoreGlobalProperties, StringComparer.OrdinalIgnoreCase);
        foreach (var kvp in additionalRestoreProperties)
        {
            newRestoreProperties[kvp.Key] = kvp.Value;
        }
        return new MSBuildArgs(
            GlobalProperties,
            new(newRestoreProperties),
            RequestedTargets,
            GetProperty,
            GetItem,
            GetTargetResult,
            GetResultOutputFile,
            Verbosity,
            OtherMSBuildArgs.ToArray());
    }
 
    public MSBuildArgs CloneWithAdditionalProperties(ReadOnlyDictionary<string, string>? additionalProperties)
    {
        if (additionalProperties is null || additionalProperties.Count == 0)
        {
            // If there are no additional properties, we can just return the current instance.
            return new MSBuildArgs(
                GlobalProperties,
                RestoreGlobalProperties,
                RequestedTargets,
                GetProperty,
                GetItem,
                GetTargetResult,
                GetResultOutputFile,
                Verbosity,
                OtherMSBuildArgs.ToArray());
        }
        if (GlobalProperties is null)
        {
            return new MSBuildArgs(
                additionalProperties,
                RestoreGlobalProperties,
                RequestedTargets,
                GetProperty,
                GetItem,
                GetTargetResult,
                GetResultOutputFile,
                Verbosity,
                OtherMSBuildArgs.ToArray());
        }
 
        var newProperties = new Dictionary<string, string>(GlobalProperties, StringComparer.OrdinalIgnoreCase);
        foreach (var kvp in additionalProperties)
        {
            newProperties[kvp.Key] = kvp.Value;
        }
        return new MSBuildArgs(
            new(newProperties),
            RestoreGlobalProperties,
            RequestedTargets,
            GetProperty,
            GetItem,
            GetTargetResult,
            GetResultOutputFile,
            Verbosity,
            OtherMSBuildArgs.ToArray());
    }
 
    public MSBuildArgs CloneWithAdditionalTargets(params ReadOnlySpan<string> additionalTargets)
    {
        string[] newTargets = RequestedTargets is not null
            ? [.. RequestedTargets, .. additionalTargets]
            : [.. additionalTargets];
        return new MSBuildArgs(
            GlobalProperties,
            RestoreGlobalProperties,
            newTargets,
            GetProperty,
            GetItem,
            GetTargetResult,
            GetResultOutputFile,
            Verbosity,
            OtherMSBuildArgs.ToArray());
    }
 
    public MSBuildArgs CloneWithVerbosity(VerbosityOptions newVerbosity)
    {
        return new MSBuildArgs(
            GlobalProperties,
            RestoreGlobalProperties,
            RequestedTargets,
            GetProperty,
            GetItem,
            GetTargetResult,
            GetResultOutputFile,
            newVerbosity,
            OtherMSBuildArgs.ToArray());
    }
 
    /// <summary>
    /// This mutates the <see cref="MSBuildArgs"/> instance, applying all of the current global properties
    /// to the restore properties dictionary. This is necessary because MSBuild's processing of restore properties
    /// is _exclusive_ - as soon as it sees a <c>-rp</c> flag, it will not apply any <c>-p</c> flags
    /// to the implicit restore operation.
    /// </summary>
    public void ApplyPropertiesToRestore()
    {
        if (RestoreGlobalProperties is null)
        {
            RestoreGlobalProperties = GlobalProperties;
            return;
        }
        else if (GlobalProperties is not null && GlobalProperties.Count > 0)
        {
            // If we have restore properties, we need to merge the global properties into them.
            // We ensure the restore properties overwrite the properties to align with expected MSBuild semantics.
            var newdict = new Dictionary<string, string>(GlobalProperties, StringComparer.OrdinalIgnoreCase);
            foreach (var restoreKvp in RestoreGlobalProperties)
            {
                newdict[restoreKvp.Key] = restoreKvp.Value;
            }
            RestoreGlobalProperties = new(newdict);
        }
    }
}