Invoke-DotnetMSBuild.ps1

# https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-command-line-reference

# .SYNOPSIS
# Builds a project and all of its dependencies. Note: A solution or project file may need to be
# specified if there are multiple.
# .DESCRIPTION
# The `Invoke-DotnetMSBuild` cmdlet allows access to a fully functional MSBuild. The command has
# the exact same capabilities as the existing MSBuild command-line client for SDK-style projects
# only. The options are all the same. For more information about the available options, see the
# MSBuild command-line reference. The `Invoke-DotnetBuild` cmdlet is equivalent to `Invoke-DotnetMSBuild
# -restore`. When you don't want to build the project and you have a specific target you want to
# run, use `Invoke-DotnetBuild` or `Invoke-DotnetMSBuild` and specify the target.
function Invoke-DotnetMSBuild {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        # Builds the targets in the project file that you specify. You can also specify a Visual
        # Studio solution file for this parameter.
        [Parameter(Mandatory, Position = 0)]
        [string]$ProjectFile,

        # Show detailed information at the end of the build log about the configurations that were
        # built and how they were scheduled to nodes.
        [Alias("DS")]
        [Parameter()]
        [ValidateSet($null, $true, $false)]
        [object]$DetailedSummary,

        # Causes MSBuild to construct and build a project graph. Constructing a graph involves
        # identifying project references to form dependencies. Building that graph involves
        # attempting to build project references prior to the projects that reference them,
        # differing from traditional MSBuild scheduling.
        # Requires MSBuild 16 or later.
        [Alias("Graph")]
        [Parameter()]
        [ValidateSet($null, $true, $false)]
        [object]$GraphBuild,

        # Ignore the specified extensions when determining which project file to build.
        [Alias("Ignore")]
        [Parameter()]
        [string[]]$IgnoreProjectExtensions,

        # List of input cache files that MSBuild will read build results from. If
        # `-IsolateProjects` is set to `False`, this sets it to `True`.
        [Alias("IRC")]
        [Parameter()]
        [string[]]$InputResultsCaches,

        # Indicates that actions in the build are allowed to interact with the user. Do not use
        # this argument in an automated scenario where interactivity is not expected. Use the
        # parameter to override a value that comes from a response file.
        [Parameter()]
        [ValidateSet($null, $true, $false)]
        [object]$Interactive,

        # Causes MSBuild to build each project in isolation. When set to `MessageUponIsolationViolation`
        # (or its short form `Message`), only the results from top-level targets are serialized if
        # the `-OutputResultsCache` switch is supplied. This is to mitigate the chances of an
        # isolation-violating target on a dependency project using incorrect state due to its
        # dependency on a cached target whose side effects would not be taken into account. (For
        # example, the definition of a property.) This is a more restrictive mode of MSBuild as it
        # requires that the project graph be statically discoverable at evaluation time, but can
        # improve scheduling and reduce memory overhead when building a large set of projects.
        [Alias("Isolate")]
        [Parameter()]
        [string]$IsolateProjects,

        # Causes MSBuild to run at low process priority.
        [Alias("Low")]
        [Parameter()]
        [ValidateSet($null, $true, $false)]
        [object]$LowPriority = $null,

        # Specifies the maximum number of concurrent processes to use when building. If you don't
        # include this parameter, the default value is 1. If you include this parameter and
        # specify `$true`, MSBuild will use up to the number of processors in the computer.
        [Alias("M")]
        [Parameter()]
        [ValidateScript({ ($_ -is [bool] -and $_ -eq $true) -or ($_ -is [int] -and $_ -gt 0) })]
        [object]$MaxCpuCount,

        # Don't include any MSBuild.rsp or Directory.Build.rsp files automatically.
        [Alias("NoAutoRsp")]
        [Parameter()]
        [switch]$NoAutoResponse,

        # Enable or disable the re-use of MSBuild nodes. You can specify the following values:
        # $true. Nodes remain after the build finishes so that subsequent builds can use them (default).
        # $false. Nodes don't remain after the build completes.
        # A node corresponds to a project that's executing. If you include the `-MaxCpuCount`
        # switch, multiple nodes can execute concurrently.
        [Alias("NR")]
        [Parameter()]
        [ValidateSet($null, $true, $false)]
        [object]$NodeReuse,

        # Don't display the startup banner or the copyright message.
        [Parameter()]
        [switch]$NoLogo,

        # Create a single, aggregated project file by inlining all the files that would be imported
        # during a build, with their boundaries marked. You can use this switch to more easily
        # determine which files are being imported, from where the files are being imported, and
        # which files contribute to the build. When you use this switch, the project isn't built.
        # If you specify a filepath, the aggregated project file is output to the file. Otherwise,
        # the output appears in the console window.
        [Alias("PP")]
        [Parameter()]
        [ValidateScript({ ($_ -is [bool] -and $_ -eq $true) -or $_ -is [string] })]
        [object]$Preprocess,

        # Output cache file where MSBuild will write the contents of its build result caches at the
        # end of the build. If `-IsolateProjects` is not set, this sets it.
        [Alias("ORC")]
        [Parameter()]
        [ValidateScript({ ($_ -is [bool] -and $_ -eq $true) -or $_ -is [string] })]
        [object]$OutputResultsCache,

        # Profiles MSBuild evaluation and writes the result to the specified file. If the extension
        # of the specified file is '.md', the result is generated in Markdown format. Otherwise, a
        # tab-separated file is produced.
        [Parameter()]
        [string]$ProfileEvaluation,

        # Set or override the specified project-level properties.
        [Alias("P")]
        [Parameter()]
        [hashtable]$Properties,

        # Runs the `Restore` target prior to building the actual targets.
        [Alias("R")]
        [Parameter()]
        [switch]$Restore,

        # Set or override these project-level properties only during restore and do not use
        # properties specified with the `-Properties` parameter.
        [Alias("RP")]
        [Parameter()]
        [hashtable]$RestoreProperties,

        # Build the specified targets in the project. If you specify any targets by using this
        # parameter, they are run instead of any targets in the `DefaultTargets` attribute in the
        # project file.
        [Alias("T")]
        [Parameter()]
        [string[]]$Target,

        # Write the list of available targets to the specified file (or the output device, if no
        # file is specified), without actually executing the build process.
        [Alias("TS")]
        [Parameter()]
        [ValidateScript({ ($_ -is [bool] -and $_ -eq $true) -or $_ -is [string] })]
        [object]$Targets,

        # Specifies a custom toolset. A toolset consists of tasks, targets, and tools that are used
        # to build an application.
        [Alias("TV")]
        [Parameter()]
        [string]$ToolsVersion,

        # Validate the project file and, if validation succeeds, build the project. If you don't
        # specify schema, the project is validated against the default schema. If you specify a
        # schema, the project is validated against the schema that you specify.
        [Alias("Val")]
        [Parameter()]
        [ValidateScript({ ($_ -is [bool] -and $_ -eq $true) -or $_ -is [string] })]
        [object]$Validate,

        # Specifies the amount of information to display in the build log. Each logger displays
        # events based on the verbosity level that you set for that logger. you can specify the
        # following verbosity levels: `Q[uiet]`, `M[inimal]`, `N[ormal]` (default), `D[etailed]`,
        # and `Diag[nostic]`.
        [Alias("V")]
        [Parameter()]
        [string]$Verbosity,

        # Display version information only. The project isn't built.
        [Alias("Ver")]
        [Parameter()]
        [switch]$Version,

        # List of warning codes to treats as errors. To treat all warnings as errors, use the
        # parameters with `$true`. When a warning is treated as an error the target continues
        # to execute as if it was a warning but the overall build fails.
        [Parameter()]
        [ValidateScript({ ($_ -is [bool] -and $_ -eq $true) -or $_ -is [string] -or $_ -is [string[]] })]
        [object]$WarnAsError,

        # List of warning codes that should not be promoted to errors. Specifically, if the
        # `-WarnAsError` parameter is set to promote all warnings to errors, error codes specified
        # with `-WarnNotAsError` are not promoted. This has no effect if `-WarnAsError` is not set
        # to promote all warnings to errors.

        [Parameter()]
        [ValidateScript({ ($_ -is [bool] -and $_ -eq $true) -or $_ -is [string] -or $_ -is [string[]] })]
        [object]$WarnNotAsError,

        # List of warning codes to treats as low importance messages.
        [Parameter()]
        [ValidateScript({ ($_ -is [bool] -and $_ -eq $true) -or $_ -is [string] -or $_ -is [string[]] })]
        [object]$WarnAsMessage,

        # Serializes all build events to a compressed binary file. By default the file is in the
        # current directory and named msbuild.binlog.
        [Parameter()]
        [ValidateScript({ ($_ -is [bool] -and $_ -eq $true) -or $_ -is [string] })]
        [object]$BinaryLogger,

        # The binary logger by default collects the source text of project files, including all
        # imported projects and target files encountered during the build. The optional
        # `-BinaryLoggerProjectImports` parameter controls this behavior. The default setting for
        # `-BinaryLoggerProjectImports` is `Embed`.
        [Alias("BL")]
        [Parameter()]
        [ValidateSet("Embed", "None", "ZipFile")]
        [string]$BinaryLoggerProjectImports,

        # Pass the parameters that you specify to the console logger, which displays build
        # information in the console window.
        [Alias("CLP")]
        [Parameter()]
        [hashtable]$ConsoleLoggerParameters,

        [Parameter(ValueFromRemainingArguments)]
        [string[]]$MSBuildArguments
    )

    $Arguments = @($ProjectFile)

    if ($DetailedSummary -is [bool]) {
        if ($DetailedSummary) {
            $Arguments += "-detailedSummary:True"
        }
        else {
            $Arguments += "-detailedSummary:False"
        }
    }

    if ($GraphBuild -is [bool]) {
        if ($GraphBuild) {
            $Arguments += "-graphBuild:True"
        }
        else {
            $Arguments += "-graphBuild:False"
        }
    }

    if ($IgnoreProjectExtensions.Count -gt 0) {
        $Arguments += "-ignoreProjectExtensions:$([string]::Join(";", $IgnoreProjectExtensions))"
    }

    if ($InputResultsCaches.Count -gt 0) {
        # Must use semicolon separator
        $Arguments += "-inputResultsCaches:$([string]::Join(";", $InputResultsCaches))"
    }

    if ($Interactive -is [bool]) {
        if ($Interactive) {
            $Arguments += "-interactive:True"
        }
        else {
            $Arguments += "-interactive:False"
        }
    }

    if ($IsolateProjects) {
        $Arguments += "-isolateProjects:$IsolateProjects"
    }

    if ($LowPriority -is [bool]) {
        if ($LowPriority) {
            $Arguments += "-lowPriority:True"
        }
        else {
            $Arguments += "-lowPriority:False"
        }
    }

    if ($MaxCpuCount -is [bool]) {
        $Arguments += "-maxCpuCount"
    }
    elseif ($MaxCpuCount -is [int]) {
        $Arguments += "-maxCpuCount:$MaxCpuCount"
    }

    if ($NoAutoResponse) {
        $Arguments += "-noAutoResponse"
    }

    if ($NodeReuse -is [bool]) {
        if ($NodeReuse) {
            $Arguments += "-nodeReuse:True"
        }
        else {
            $Arguments += "-nodeReuse:False"
        }
    }

    if ($NoLogo) {
        $Arguments += "-nologo"
    }

    if ($Preprocess -is [bool]) {
        $Arguments += "-preprocess"
    }
    elseif ($Preprocess -is [string]) {
        $Arguments += "-preprocess:$Preprocess"
    }

    if ($OutputResultsCache -is [bool]) {
        $Arguments += "-outputResultsCache"
    }
    elseif ($OutputResultsCache -is [string]) {
        $Arguments += "-outputResultsCache:$OutputResultsCache"
    }

    if ($ProfileEvaluation) {
        $Arguments += "-profileEvaluation:$ProfileEvaluation"
    }

    if ($Properties) {
        foreach ($property in ($Properties.GetEnumerator() | Sort-Object Name)) {
            $Arguments += "-property:$($property.Name)=$($property.Value)"
        }
    }

    if ($Restore) {
        $Arguments += "-restore"
    }

    if ($RestoreProperties) {
        foreach ($restoreProperty in ($RestoreProperties.GetEnumerator() | Sort-Object Name)) {
            $Arguments += "-restoreProperty:$($restoreProperty.Name)=$($restoreProperty.Value)"
        }
    }

    if ($Target) {
        $Arguments += "-target:$([string]::Join(",", $Target))"
    }

    if ($Targets -is [bool]) {
        $Arguments += "-targets"
    }
    elseif ($Targets -is [string]) {
        $Arguments += "-targets:$Targets"
    }

    if ($ToolsVersion) {
        $Arguments += "-toolsVersion:$ToolsVersion"
    }

    if ($Validate -is [bool]) {
        $Arguments += "-validate"
    }
    elseif ($Validate -is [string]) {
        $Arguments += "-validate:$Validate"
    }

    if ($Verbosity) {
        $Arguments += "-verbosity:$Verbosity"
    }

    if ($Version) {
        $Arguments += "-version"
    }

    if ($WarnAsError -is [bool]) {
        $Arguments += "-warnAsError"
    }
    elseif ($WarnAsError -is [string]) {
        $Arguments += "-warnAsError:$WarnAsError"
    }
    elseif ($WarnAsError -is [string[]]) {
        $Arguments += "-warnAsError:$([string]::Join(";", $WarnAsError))"
    }

    if ($WarnNotAsError -is [bool]) {
        $Arguments += "-warnNotAsError"
    }
    elseif ($WarnNotAsError -is [string]) {
        $Arguments += "-warnNotAsError:$WarnNotAsError"
    }
    elseif ($WarnNotAsError -is [array]) {
        $Arguments += "-warnNotAsError:$([string]::Join(";", $WarnNotAsError))"
    }

    if ($WarnAsMessage -is [bool]) {
        $Arguments += "-warnAsMessage"
    }
    elseif ($WarnAsMessage -is [string]) {
        $Arguments += "-warnAsMessage:$WarnAsMessage"
    }
    elseif ($WarnAsMessage -is [string[]]) {
        $Arguments += "-warnAsMessage:$([string]::Join(";", $WarnAsMessage))"
    }

    if ($null -ne $BinaryLogger) {
        $argument = "-binaryLogger"
        if ($BinaryLogger -is [string]) {
            $argument += ":LogFile=$BinaryLogger"
        }

        if ($null -ne $BinaryLoggerProjectImports) {
            $argument += ";ProjectImports=$BinaryLoggerProjectImports"
        }

        $Arguments += $argument
    }

    if ($ConsoleLoggerParameters -and $ConsoleLoggerParameters.Count -gt 0) {
        $argument = "-consoleLoggerParameters:"
        foreach ($parameter in ($ConsoleLoggerParameters.GetEnumerator() | Sort-Object Name)) {
            if ($parameter.Value -is [bool] -and $parameter.Value -eq $true) {
                $argument += "$($parameter.Name);"
            }
            else {
                $argument += "$($parameter.Name)=$($parameter.Value);"
            }
        }

        $argument = $argument.TrimEnd(";")
        $Arguments += $argument
    }

    Invoke-Dotnet "msbuild" @Arguments @MSBuildArguments
}