Resolve-MSBuild.ps1


<#PSScriptInfo
.VERSION 1.1.1
.AUTHOR Roman Kuzmin
.COPYRIGHT (c) Roman Kuzmin
.TAGS Invoke-Build, MSBuild
.GUID 53c01926-4fc5-4cbd-aa46-32e415b2373b
.LICENSEURI http://www.apache.org/licenses/LICENSE-2.0
.PROJECTURI https://github.com/nightroman/Invoke-Build
#>


<#
.Synopsis
    Finds the specified or latest MSBuild.
 
.Description
    The script finds the path to the specified or latest version of MSBuild.
    It is designed to work for MSBuild 2.0-15.0 and support future versions.
 
    For MSBuild 15.0+ the command uses VSSetup module from PSGallery.
    If it is not installed then some default locations are checked.
    Thus, VSSetup module is required for not default installations.
 
    MSBuild 15.0+ resolution precedence: Enterprise, Professional, Community,
    another product. If this is not suitable then use VSSetup module directly.
 
    For MSBuild 2.0-14.0 the information is taken from the registry.
 
.Parameter Version
        Specifies the required MSBuild version. If it is omitted, empty, or *
        then the command finds and returns the latest installed version path.
 
.Outputs
    The full path to MSBuild.exe
 
.Example
    Resolve-MSBuild 15.0
    Gets location of MSBuild installed with Visual Studio 2017.
 
.Link
    https://www.powershellgallery.com/packages/VSSetup
#>


[CmdletBinding()]
param(
    [string]$Version
)

function Get-MSBuild15VSSetup {
    if (!(Get-Module VSSetup -ListAvailable)) {return}
    Import-Module VSSetup

    $vs = Get-VSSetupInstance | Select-VSSetupInstance -Version 15.0 -Require Microsoft.Component.MSBuild -Product *
    if (!$vs) {return}

    $vs = if ($r = $vs | Select-VSSetupInstance -Product Microsoft.VisualStudio.Product.Enterprise) {$r}
    elseif ($r = $vs | Select-VSSetupInstance -Product Microsoft.VisualStudio.Product.Professional) {$r}
    elseif ($r = $vs | Select-VSSetupInstance -Product Microsoft.VisualStudio.Product.Community) {$r}
    else {$vs}

    if ($vs) {
        Join-Path @($vs)[0].InstallationPath MSBuild\15.0\Bin\MSBuild.exe
    }
}

function Get-MSBuild15Guess {
    if (!($root = ${env:ProgramFiles(x86)})) {$root = $env:ProgramFiles}
    if (!(Test-Path -LiteralPath "$root\Microsoft Visual Studio\2017")) {return}

    $paths = @(
        foreach($_ in Resolve-Path "$root\Microsoft Visual Studio\2017\*\MSBuild\15.0\Bin\MSBuild.exe" -ErrorAction 0) {
            $_.ProviderPath
        }
    )

    if ($paths) {
        if ($r = $paths -like '*\Enterprise\*') {return $r}
        if ($r = $paths -like '*\Professional\*') {return $r}
        if ($r = $paths -like '*\Community\*') {return $r}
        $paths[0]
    }
}

function Get-MSBuild15 {
    if ($path = Get-MSBuild15VSSetup) {
        $path
    }
    else {
        Get-MSBuild15Guess
    }
}

function Get-MSBuildOldLatest {
    $rp = @(Get-ChildItem HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions | Sort-Object {[Version]$_.PSChildName})
    if ($rp) {
        Join-Path ($rp[-1].GetValue('MSBuildToolsPath')) MSBuild.exe
    }
}

function Get-MSBuildOldVersion($Version) {
    $rp = [Microsoft.Win32.Registry]::GetValue("HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSBuild\ToolsVersions\$Version", 'MSBuildToolsPath', '')
    if ($rp) {
        Join-Path $rp MSBuild.exe
    }
}

$ErrorActionPreference = 1
try {
    $v15 = [Version]'15.0'
    $vMax = [Version]'9999.0'
    if (!$Version) {$Version = '*'}
    $vRequired = if ($Version -eq '*') {$vMax} else {[Version]$Version}

    if ($vRequired -eq $v15) {
        if ($path = Get-MSBuild15) {
            return $path
        }
    }
    elseif ($vRequired -lt $v15) {
        if ($path = Get-MSBuildOldVersion $Version) {
            return $path
        }
    }
    elseif ($vRequired -eq $vMax) {
        if ($path = Get-MSBuild15) {
            return $path
        }
        if ($path = Get-MSBuildOldLatest) {
            return $path
        }
    }

    throw 'The specified version is not found.'
}
catch {
    Write-Error "Cannot resolve MSBuild $Version : $_"
}