private/Resolve-FrameworkDirectories.ps1

# spell-checker:ignore HKLM bitness bitnesspart
function Resolve-FrameworkDirectories {
    <#
    .SYNOPSIS
    Resolves the list of directories for a given .NET framework version string.

    .DESCRIPTION
    Parses the framework string (e.g. '4.8', '4.8x64'), determines the matching
    MSBuild tool paths and/or Windows .NET runtime paths, and returns the full
    list of candidate directories. Does not verify that they exist on disk.

    Throws if the framework string is syntactically invalid.
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Framework
    )

    if ($Framework -cmatch '^((?:\d+\.\d+)(?:\.\d+){0,1})(x86|x64){0,1}$') {
        $versionPart = $matches[1]
        $bitnessPart = $matches[2]
    } else {
        throw ($msgs.error_invalid_framework -f $Framework)
    }

    $versions = $null
    $buildToolsVersions = $null
    switch ($versionPart) {
        '1.0' { $versions = @('v1.0.3705') }
        '1.1' { $versions = @('v1.1.4322') }
        '1.1.0' { $versions = @() }
        '2.0' { $versions = @('v2.0.50727') }
        '2.0.0' { $versions = @() }
        '3.0' { $versions = @('v2.0.50727') }
        '3.5' { $versions = @('v3.5', 'v2.0.50727') }
        '4.0' { $versions = @('v4.0.30319') }
        { ($_ -eq '4.5') -or ($_ -eq '4.5.1') -or ($_ -eq '4.5.2') } {
            $versions = @('v4.0.30319')
            $buildToolsVersions = @('17.0', '16.0', '15.0', '14.0', '12.0')
        }
        { ($_ -eq '4.6') -or ($_ -eq '4.6.1') -or ($_ -eq '4.6.2') } {
            $versions = @('v4.0.30319')
            $buildToolsVersions = @('17.0', '16.0', '15.0', '14.0')
        }
        { ($_ -eq '4.7') -or ($_ -eq '4.7.1') -or ($_ -eq '4.7.2') } {
            $versions = @('v4.0.30319')
            $buildToolsVersions = @('17.0', '16.0', '15.0')
        }
        { ($_ -eq '4.8') -or ($_ -eq '4.8.1') } {
            $versions = @('v4.0.30319')
            $buildToolsVersions = @('17.0', '16.0')
        }
        default {
            throw ($msgs.error_invalid_framework -f $Framework)
        }
    }

    $bitness = 'Framework'
    switch ($bitnessPart) {
        'x86' {
            $bitness = 'Framework'
            $buildToolsKey = 'MSBuildToolsPath32'
        }
        'x64' {
            $bitness = 'Framework64'
            $buildToolsKey = 'MSBuildToolsPath'
        }
        { [string]::IsNullOrEmpty($_) } {
            $ptrSize = [System.IntPtr]::Size
            switch ($ptrSize) {
                4 {
                    $bitness = 'Framework'
                    $buildToolsKey = 'MSBuildToolsPath32'
                }
                8 {
                    $bitness = 'Framework64'
                    $buildToolsKey = 'MSBuildToolsPath'
                }
                default {
                    throw ($msgs.error_unknown_pointersize -f $ptrSize)
                }
            }
        }
        default {
            throw ($msgs.error_unknown_bitnesspart -f $bitnessPart, $Framework)
        }
    }

    $frameworkDirs = @()
    if ($null -ne $buildToolsVersions) {
        foreach ($ver in $buildToolsVersions) {
            if ($ver -eq "15.0") {
                if ($null -eq (Get-Module -Name VSSetup)) {
                    if ($null -eq (Get-Module -Name VSSetup -ListAvailable)) {
                        Write-BuildMessage ($msgs.warning_missing_vsssetup_module -f $ver) "warning"
                        continue
                    }
                    Import-Module VSSetup
                }

                # borrowed from nightroman https://github.com/nightroman/Invoke-Build
                if ($vsInstances = Get-VSSetupInstance) {
                    $vs = @($vsInstances | Select-VSSetupInstance -Version '[15.0, 16.0)' -Require Microsoft.Component.MSBuild)
                    if ($vs) {
                        if ($buildToolsKey -eq 'MSBuildToolsPath32') {
                            $frameworkDirs += Join-Path ($vs[0].InstallationPath) MSBuild\15.0\Bin
                        } else {
                            $frameworkDirs += Join-Path ($vs[0].InstallationPath) MSBuild\15.0\Bin\amd64
                        }
                    }

                    $vs = @($vsInstances | Select-VSSetupInstance -Version '[15.0, 16.0)' -Product Microsoft.VisualStudio.Product.BuildTools)
                    if ($vs) {
                        if ($buildToolsKey -eq 'MSBuildToolsPath32') {
                            $frameworkDirs += Join-Path ($vs[0].InstallationPath) MSBuild\15.0\Bin
                        } else {
                            $frameworkDirs += Join-Path ($vs[0].InstallationPath) MSBuild\15.0\Bin\amd64
                        }
                    }
                } else {
                    if (!($root = ${env:ProgramFiles(x86)})) { $root = $env:ProgramFiles }
                    if (Test-Path -LiteralPath "$root\Microsoft Visual Studio\2017") {
                        if ($buildToolsKey -eq 'MSBuildToolsPath32') {
                            $rp = @(Resolve-Path "$root\Microsoft Visual Studio\2017\*\MSBuild\15.0\Bin" -ErrorAction SilentlyContinue)
                        } else {
                            $rp = @(Resolve-Path "$root\Microsoft Visual Studio\2017\*\MSBuild\15.0\Bin\amd64" -ErrorAction SilentlyContinue)
                        }
                        if ($rp) { $frameworkDirs += $rp[-1].ProviderPath }
                    }
                }
            } elseif ($ver -eq "16.0") {
                if ($null -eq (Get-Module -Name VSSetup)) {
                    if ($null -eq (Get-Module -Name VSSetup -ListAvailable)) {
                        Write-BuildMessage ($msgs.warning_missing_vsssetup_module -f $ver) "warning"
                        continue
                    }
                    Import-Module VSSetup
                }

                # borrowed from nightroman https://github.com/nightroman/Invoke-Build
                if ($vsInstances = Get-VSSetupInstance) {
                    $vs = @($vsInstances | Select-VSSetupInstance -Version '[16.0, 17.0)' -Require Microsoft.Component.MSBuild)
                    if ($vs) { $frameworkDirs += Join-Path ($vs[0].InstallationPath) MSBuild\Current\Bin }

                    $vs = @($vsInstances | Select-VSSetupInstance -Version '[16.0, 17.0)' -Product Microsoft.VisualStudio.Product.BuildTools)
                    if ($vs) { $frameworkDirs += Join-Path ($vs[0].InstallationPath) MSBuild\Current\Bin }
                } else {
                    if (!($root = ${env:ProgramFiles(x86)})) { $root = $env:ProgramFiles }
                    if (Test-Path -LiteralPath "$root\Microsoft Visual Studio\2019") {
                        $rp = @(Resolve-Path "$root\Microsoft Visual Studio\2019\*\MSBuild\Current\Bin" -ErrorAction SilentlyContinue)
                        if ($rp) { $frameworkDirs += $rp[-1].ProviderPath }
                    }
                }
            } elseif ($ver -eq "17.0") {
                if ($null -eq (Get-Module -Name VSSetup)) {
                    if ($null -eq (Get-Module -Name VSSetup -ListAvailable)) {
                        Write-BuildMessage ($msgs.warning_missing_vsssetup_module -f $ver) "Warning"
                        continue
                    }
                    Import-Module VSSetup
                }

                # borrowed from nightroman https://github.com/nightroman/Invoke-Build
                if ($vsInstances = Get-VSSetupInstance) {
                    $vs = @($vsInstances | Select-VSSetupInstance -Version '[17.0,)' -Require Microsoft.Component.MSBuild)
                    if ($vs) { $frameworkDirs += Join-Path ($vs[0].InstallationPath) MSBuild\Current\Bin }

                    $vs = @($vsInstances | Select-VSSetupInstance -Version '[17.0,)' -Product Microsoft.VisualStudio.Product.BuildTools)
                    if ($vs) { $frameworkDirs += Join-Path ($vs[0].InstallationPath) MSBuild\Current\Bin }
                } else {
                    if (!($root = ${env:ProgramFiles(x86)})) { $root = $env:ProgramFiles }
                    if (Test-Path -LiteralPath "$root\Microsoft Visual Studio\2022") {
                        $rp = @(Resolve-Path "$root\Microsoft Visual Studio\2022\*\MSBuild\Current\Bin" -ErrorAction SilentlyContinue)
                        if ($rp) { $frameworkDirs += $rp[-1].ProviderPath }
                    }
                }
            } elseif (Test-Path "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\$ver") {
                $frameworkDirs += (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\$ver" -Name $buildToolsKey).$buildToolsKey
            }
        }
    }

    $frameworkDirs = $frameworkDirs + @($versions | ForEach-Object { "$env:windir\Microsoft.NET\$bitness\$_\" })

    # Expand registry macro references, e.g. $(Registry:HKEY_LOCAL_MACHINE\...\@name)
    for ($i = 0; $i -lt $frameworkDirs.Count; $i++) {
        $dir = $frameworkDirs[$i]
        if ($dir -match "\$\(Registry:HKEY_LOCAL_MACHINE(.*?)@(.*)\)") {
            $key = "HKLM:" + $matches[1]
            $name = $matches[2]
            $frameworkDirs[$i] = (Get-ItemProperty -Path $key -Name $name).$name
        }
    }

    return $frameworkDirs
}