Eigenverft.Manifested.Drydock.Framework.ps1
|
function Get-DotNetFrameworkInfo { <# .SYNOPSIS Enumerates installed .NET Framework (1.1 → 4.8.1) and shows build-capability packs per family. .DESCRIPTION Reads NDP registry (both views on x64 OS), skips LCID/tech leaves, resolves InstallPath robustly. Adds per-row capability sets: - TargetPacksApplicable/TargetPacksApplicableTFM (by PRODUCT FAMILY: 1.1, 2.0, 3.0, 3.5, 4.x) - TargetPacksMachine/TargetPacksMachineTFM (all packs on the machine) …and version-typed variants: - TargetPacksApplicableVersion / TargetPacksApplicableVersionString - TargetPacksMachineVersion / TargetPacksMachineVersionString #> [CmdletBinding()] param( [switch]$IncludeNonInstalled, [switch]$IncludeFeatureState ) if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) { throw "Get-DotNetFrameworkInfo supports Windows only." } $is64OS = [Environment]::Is64BitOperatingSystem $views = if ($is64OS) { @([Microsoft.Win32.RegistryView]::Registry32, [Microsoft.Win32.RegistryView]::Registry64) } else { @([Microsoft.Win32.RegistryView]::Registry32) } function Get-ReleaseLabel { param([int]$Release) $map = @( @{ Release=533325; Label='4.8.1' }, @{ Release=533320; Label='4.8.1' }, @{ Release=528372; Label='4.8' }, @{ Release=528049; Label='4.8' }, @{ Release=528040; Label='4.8' }, @{ Release=461814; Label='4.7.2' }, @{ Release=461808; Label='4.7.2' }, @{ Release=461310; Label='4.7.1' }, @{ Release=461308; Label='4.7.1' }, @{ Release=460805; Label='4.7' }, @{ Release=460798; Label='4.7' }, @{ Release=394806; Label='4.6.2' }, @{ Release=394802; Label='4.6.2' }, @{ Release=394271; Label='4.6.1' }, @{ Release=394254; Label='4.6.1' }, @{ Release=393297; Label='4.6' }, @{ Release=393295; Label='4.6' }, @{ Release=379893; Label='4.5.2' }, @{ Release=378758; Label='4.5.1' }, @{ Release=378675; Label='4.5.1' }, @{ Release=378389; Label='4.5' } ) foreach ($m in $map | Sort-Object Release -Descending) { if ($Release -ge $m.Release) { return $m.Label } } if ($Release -ge 378389) { return "4.5 or later (Release=$Release)" } return $null } function Get-CanonicalFolderName { param([string]$Product) switch -regex ($Product) { '^v1\.1\.4322($|\\)' { 'v1.1.4322'; break } '^v2\.0\.50727($|\\)' { 'v2.0.50727'; break } '^v3\.0($|\\)' { 'v3.0'; break } '^v3\.5($|\\)' { 'v3.5'; break } '^v4\\' { 'v4.0.30319'; break } default { $null; break } } } function IsProductKey { param([string]$vName,[string]$leaf) if ($vName -in 'v1.1.4322','v2.0.50727','v3.0','v3.5') { return $true } if ($vName -eq 'v4' -and $leaf -in 'Client','Full') { return $true } if ($leaf -match '^\d{3,4}$') { return $false } # LCID if ($leaf -match '^(WCF|WF|WPF|XPS|Setup)$') { return $false } return $false } function ComputeClr { param([string]$product) switch -regex ($product) { '^v1\.1\.4322' { '1.1'; break } '^(v2\.0\.50727|v3\.0|v3\.5)' { '2.0'; break } default { '4.0'; break } } } # NEW: classify product → family (used for applicability scoping) function Get-Family { param([string]$product) switch -regex ($product) { '^v1\.1\.4322' { '1.1'; break } '^v2\.0\.50727' { '2.0'; break } '^v3\.0($|\\)' { '3.0'; break } '^v3\.5($|\\)' { '3.5'; break } '^v4\\' { '4.0'; break } default { $null; break } } } # ---- Capability detection (machine-wide) ---- function Get-InstalledTargetPacks { $list = New-Object System.Collections.ArrayList $windir = [Environment]::GetEnvironmentVariable('WINDIR','Machine'); if (-not $windir) { $windir = $env:WINDIR } $pf = [Environment]::GetFolderPath('ProgramFiles') $pf86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)') function Add-Pack { param([string]$name) if ($name -and -not [string]::IsNullOrWhiteSpace($name)) { if (-not ($list -contains $name)) { [void]$list.Add($name) } } } # v1.1 / v2.0: compiler presence (check both Framework and Framework64 on x64 OS) foreach ($dir in @('Framework','Framework64')) { $base = Join-Path $windir ("Microsoft.NET\{0}" -f $dir) if (Test-Path -LiteralPath (Join-Path $base 'v1.1.4322\csc.exe')) { Add-Pack 'v1.1.4322' } if (Test-Path -LiteralPath (Join-Path $base 'v2.0.50727\csc.exe')) { Add-Pack 'v2.0' } } # v3.0 / v3.5 reference assemblies (legacy) foreach ($base in @( ($(if($pf86){ Join-Path $pf86 'Reference Assemblies\Microsoft\Framework' })), (Join-Path $pf 'Reference Assemblies\Microsoft\Framework') )) { if ($base -and (Test-Path -LiteralPath $base)) { if (Test-Path -LiteralPath (Join-Path $base 'v3.0')) { Add-Pack 'v3.0' } if (Test-Path -LiteralPath (Join-Path $base 'v3.5')) { Add-Pack 'v3.5' } } } # Fallback: MSBuild 3.5 presence foreach ($dir in @('Framework','Framework64')) { $fx35 = Join-Path $windir ("Microsoft.NET\{0}\v3.5\MSBuild.exe" -f $dir) if (Test-Path -LiteralPath $fx35) { Add-Pack 'v3.5' } } # v4.x Developer Packs (.NETFramework ref assemblies) foreach ($base in @( ($(if($pf86){ Join-Path $pf86 'Reference Assemblies\Microsoft\Framework\.NETFramework' })), (Join-Path $pf 'Reference Assemblies\Microsoft\Framework\.NETFramework') )) { if ($base -and (Test-Path -LiteralPath $base)) { Get-ChildItem -LiteralPath $base -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^v4(\.\d+)*$' } | ForEach-Object { Add-Pack $_.Name } } } # Sort by version $sorted = $list | Sort-Object { $v = $_.TrimStart('v') try { [version]$v } catch { [version]'0.0' } } # TFM map $mapTFM = @{ 'v1.1.4322'='net11'; 'v2.0'='net20'; 'v3.0'='net30'; 'v3.5'='net35'; 'v4.0'='net40'; 'v4.5'='net45'; 'v4.5.1'='net451'; 'v4.5.2'='net452'; 'v4.6'='net46'; 'v4.6.1'='net461'; 'v4.6.2'='net462'; 'v4.7'='net47'; 'v4.7.1'='net471'; 'v4.7.2'='net472'; 'v4.8'='net48'; 'v4.8.1'='net481' } # Group by CLR (kept for completeness; no longer used for applicability) function PackClr { param([string]$p) if ($p -eq 'v1.1.4322') { return '1.1' } if ($p -in @('v2.0','v3.0','v3.5')) { return '2.0' } if ($p -like 'v4*') { return '4.0' } return $null } $byClr = @{ '1.1'=@(); '2.0'=@(); '4.0'=@() } foreach ($p in $sorted) { $c = PackClr $p; if ($c) { $byClr[$c] = $byClr[$c] + @($p) } } [pscustomobject]@{ AllPacks = [string[]]$sorted AllTFM = [string[]]($sorted | ForEach-Object { if ($mapTFM.ContainsKey($_)) { $mapTFM[$_] } }) ByClr = $byClr PackToTfmMap = $mapTFM } } # Convert pack names ('v3.5') to [version] (3.5) + string ('3.5') function Convert-PacksToVersionForms { param([string[]]$packs) $verList = New-Object System.Collections.ArrayList $strList = New-Object System.Collections.ArrayList foreach ($p in ($packs | Where-Object { $_ })) { $s = $p.TrimStart('v') if ($s -eq '4') { $s = '4.0' } try { $v = [version]$s [void]$verList.Add($v) [void]$strList.Add($s) } catch { } } [pscustomobject]@{ Versions = [version[]]$verList Strings = [string[]]$strList } } $cap = Get-InstalledTargetPacks # Optional NetFx3 state $netFx3State = $null if ($IncludeFeatureState) { try { if (Get-Command -Name Get-WindowsOptionalFeature -ErrorAction SilentlyContinue) { $fx = Get-WindowsOptionalFeature -Online -FeatureName NetFx3 -ErrorAction Stop $netFx3State = $fx.State } else { $out = & dism.exe /Online /Get-FeatureInfo /FeatureName:NetFx3 2>$null $line = $out | Select-String -Pattern 'State\s*:\s*(.+)' | Select-Object -First 1 if ($line) { $netFx3State = ($line.Matches.Groups[1].Value).Trim() } } } catch { Write-Verbose ("Failed to query NetFx3 feature state: {0}" -f $_.Exception.Message) } } # Path bases $windir = [Environment]::GetEnvironmentVariable('WINDIR','Machine'); if (-not $windir) { $windir = $env:WINDIR } $fwBase32 = Join-Path $windir 'Microsoft.NET\Framework' $fwBase64 = Join-Path $windir 'Microsoft.NET\Framework64' $results = foreach ($view in $views) { try { $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $view) $installRoot = $null try { $irKey = $base.OpenSubKey('SOFTWARE\Microsoft\.NETFramework') if ($irKey) { $installRoot = $irKey.GetValue('InstallRoot', $null) } } catch { Write-Verbose ("Failed reading InstallRoot in {0} view: {1}" -f $view, $_.Exception.Message) } $ndp = $base.OpenSubKey('SOFTWARE\Microsoft\NET Framework Setup\NDP') if (-not $ndp) { continue } foreach ($vName in $ndp.GetSubKeyNames()) { if ($vName -notmatch '^v\d') { continue } $vKey = $ndp.OpenSubKey($vName); if (-not $vKey) { continue } $leaves = @() if ($vName -eq 'v4') { foreach ($leafName in $vKey.GetSubKeyNames()) { if (IsProductKey -vName $vName -leaf $leafName) { $leaves += $leafName } } } else { $leaves += '' } foreach ($leaf in $leaves) { $k = if ($leaf) { $vKey.OpenSubKey($leaf) } else { $vKey } if (-not $k) { continue } $ver = $k.GetValue('Version') $rel = $k.GetValue('Release') $installFlag = $k.GetValue('Install', $null) $sp = $k.GetValue('SP', $null) $rawInstallPath = $k.GetValue('InstallPath', $null) $installSuccess = $k.GetValue('InstallSuccess', $null) if ($vName -eq 'v3.0' -and -not $installSuccess) { $setupKey = $k.OpenSubKey('Setup'); if ($setupKey) { $installSuccess = $setupKey.GetValue('InstallSuccess', $null) } } $isInstalled = if ($installFlag -is [int]) { $installFlag -eq 1 } elseif ($rel) { $true } else { $false } if (-not $IncludeNonInstalled -and -not $isInstalled) { continue } $product = if ($leaf) { "$vName\$leaf" } else { $vName } $clr = ComputeClr -product $product $releaseLabel = if ($rel) { Get-ReleaseLabel -Release $rel } else { $null } $legacyLabel = switch -regex ($product) { '^v1\.1\.4322' { '1.1' + ($(if($sp){ " SP$sp"})); break } '^v2\.0\.50727' { '2.0' + ($(if($sp){ " SP$sp"})); break } '^v3\.0' { '3.0' + ($(if($sp){ " SP$sp"})); break } '^v3\.5' { '3.5' + ($(if($sp){ " SP$sp"})); break } '^v4\\' { if (-not $rel) { '4.0' }; break } default { $null; break } } $label = if ($releaseLabel) { $releaseLabel } else { $legacyLabel } # InstallPath (candidate selection) $canonFolder = Get-CanonicalFolderName -Product $product $candidates = New-Object System.Collections.Generic.List[object] if ($rawInstallPath) { $candidates.Add([pscustomobject]@{ Path=$rawInstallPath; Source='Registry'; Exists=(Test-Path -LiteralPath $rawInstallPath -PathType Container) }) | Out-Null } if ($installRoot -and $canonFolder) { $p = Join-Path $installRoot $canonFolder $candidates.Add([pscustomobject]@{ Path=$p; Source='InstallRoot'; Exists=(Test-Path -LiteralPath $p -PathType Container) }) | Out-Null } if ($canonFolder) { $p32 = Join-Path $fwBase32 $canonFolder $p64 = Join-Path $fwBase64 $canonFolder $candidates.Add([pscustomobject]@{ Path=$p32; Source='WinDir\Framework'; Exists=(Test-Path -LiteralPath $p32 -PathType Container) }) | Out-Null $candidates.Add([pscustomobject]@{ Path=$p64; Source='WinDir\Framework64'; Exists=(Test-Path -LiteralPath $p64 -PathType Container) }) | Out-Null } $preferred = $null if ($view -eq [Microsoft.Win32.RegistryView]::Registry64) { $preferred = $candidates | Where-Object { $_.Exists -and ($_.Path -like "*\Framework64\*") } | Select-Object -First 1 } else { $preferred = $candidates | Where-Object { $_.Exists -and ($_.Path -like "*\Framework\*" -and $_.Path -notlike "*\Framework64\*") } | Select-Object -First 1 } if (-not $preferred) { $preferred = $candidates | Where-Object { $_.Exists } | Select-Object -First 1 } if (-not $preferred) { $preferred = $candidates | Select-Object -First 1 } # --- Capability filtering by PRODUCT FAMILY (not CLR) --- $family = Get-Family -product $product switch ($family) { '1.1' { $packsApplicable = @('v1.1.4322') } '2.0' { $packsApplicable = @('v2.0') } '3.0' { $packsApplicable = @('v3.0') } '3.5' { $packsApplicable = @('v3.5') } '4.0' { $packsApplicable = @($cap.AllPacks | Where-Object { $_ -like 'v4*' }) } default { $packsApplicable = @() } } # Keep only packs actually present on this machine $packsApplicable = @($packsApplicable | Where-Object { $cap.AllPacks -contains $_ }) # Map to TFM + version-typed forms $packsApplicableTFM = @() foreach ($p in $packsApplicable) { if ($cap.PackToTfmMap.ContainsKey($p)) { $packsApplicableTFM += $cap.PackToTfmMap[$p] } } $appVer = Convert-PacksToVersionForms -packs $packsApplicable $allVer = Convert-PacksToVersionForms -packs $cap.AllPacks # ------------------------------------------------------ [pscustomobject]@{ Product = $product Profile = if ($product -like 'v4*') { ($product -split '\\',2)[1] } else { $null } CLR = $clr Version = $ver Release = $rel ReleaseLabel = $releaseLabel Label = $label ServicePack = $sp Install = [bool]$isInstalled InstallSuccess = $installSuccess InstallPath = if ($preferred) { $preferred.Path } else { $null } InstallPathSource = if ($preferred) { $preferred.Source } else { $null } DirectoryExists = if ($preferred) { [bool]$preferred.Exists } else { $false } NetFx3FeatureState = if ($IncludeFeatureState -and ($product -in 'v2.0.50727','v3.0','v3.5')) { $netFx3State } else { $null } RegistryView = if ($view -eq [Microsoft.Win32.RegistryView]::Registry64) { '64-bit' } else { '32-bit' } RegistryKey = $k.Name TargetPacksApplicable = $packsApplicable TargetPacksApplicableTFM = $packsApplicableTFM TargetPacksMachine = $cap.AllPacks TargetPacksMachineTFM = $cap.AllTFM TargetPacksApplicableVersion = $appVer.Versions TargetPacksApplicableVersionString = $appVer.Strings TargetPacksMachineVersion = $allVer.Versions TargetPacksMachineVersionString = $allVer.Strings } } } } catch { Write-Verbose ("Failed reading registry view {0}: {1}" -f $view, $_.Exception.Message) } } $results | Sort-Object Product, RegistryView } function Get-DotNetFrameworkLatestByFamily { <# .SYNOPSIS Return the full enumeration from Get-DotNetFrameworkInfo and, per family (1.1, 2.0, 3.0, 3.5, 4.0), the single “latest” installed row, preferring the highest bitness on this OS. .DESCRIPTION - Calls Get-DotNetFrameworkInfo with only supported switches. - Families: v1.1.4322 → 1.1, v2.0.50727 → 2.0, v3.0 → 3.0, v3.5 → 3.5, v4\* → 4.0. - Bitness: on x64 OS, choose 64-bit rows if any installed; otherwise fall back to 32-bit. On x86 OS: 32-bit only. - Ranking within a family: * 4.0-family: Release (desc), Profile (Full > Client), Version ([version] desc), DirectoryExists. * 2.0/3.0/3.5/1.1: ServicePack (desc), Version ([version] desc), DirectoryExists. - Output: * .All — all rows from Get-DotNetFrameworkInfo (unchanged) * .LatestByFamily — the chosen rows, with an added .Family property #> [CmdletBinding()] param( [switch]$IncludeNonInstalled, [switch]$IncludeFeatureState ) # Call enumerator with only supported switches (no HighestBitness leakage) $all = if ($IncludeNonInstalled -or $IncludeFeatureState) { Get-DotNetFrameworkInfo -IncludeNonInstalled:$IncludeNonInstalled -IncludeFeatureState:$IncludeFeatureState } else { Get-DotNetFrameworkInfo } if (-not $all) { return [pscustomobject]@{ All=@(); LatestByFamily=@() } } # Family classifier (keep regexes precise) function Get-Family { param([string]$product) switch -regex ($product) { '^v4\\' { '4.0'; break } '^v3\.5($|\\)' { '3.5'; break } '^v3\.0($|\\)' { '3.0'; break } '^v2\.0\.50727' { '2.0'; break } '^v1\.1\.4322' { '1.1'; break } default { $null; break } } } $is64 = [Environment]::Is64BitOperatingSystem # Bucket rows by family $byFamily = @{} foreach ($r in $all) { $fam = Get-Family -product $r.Product if (-not $fam) { continue } if (-not $IncludeNonInstalled -and -not $r.Install) { continue } # only installed by default if (-not $byFamily.ContainsKey($fam)) { $byFamily[$fam] = New-Object System.Collections.ArrayList } [void]$byFamily[$fam].Add($r) } $selected = foreach ($fam in $byFamily.Keys | Sort-Object) { $candidates = [System.Collections.ArrayList]$byFamily[$fam] if (-not $candidates -or $candidates.Count -eq 0) { continue } # Prefer highest bitness available (x64 on x64 OS; otherwise x86) if ($is64) { $x64 = $candidates | Where-Object { $_.RegistryView -eq '64-bit' -and $_.Install } if ($x64) { $candidates = [System.Collections.ArrayList]@($x64) } else { $x86 = $candidates | Where-Object { $_.RegistryView -eq '32-bit' -and $_.Install } if ($x86) { $candidates = [System.Collections.ArrayList]@($x86) } } } else { $x86 = $candidates | Where-Object { $_.RegistryView -eq '32-bit' -and $_.Install } if ($x86) { $candidates = [System.Collections.ArrayList]@($x86) } } # Rank within family $ranked = $candidates | Select-Object *, @{n='ReleaseRank';e={ if ($_.Release) { [int]$_.Release } else { -1 } }}, @{n='ProfileRank';e={ if ($fam -eq '4.0') { if ($_.Profile -eq 'Full') { 1 } elseif ($_.Profile -eq 'Client') { 0 } else { -1 } } else { -1 } }}, @{n='ServicePackRank';e={ if ($_.ServicePack -is [int]) { [int]$_.ServicePack } else { 0 } }}, @{n='VersionRank';e={ try { [version]($_.Version) } catch { [version]'0.0' } }}, @{n='DirRank';e={ if ($_.DirectoryExists) { 1 } else { 0 } }} | Sort-Object ` @{e={ if ($fam -eq '4.0') { $_.ReleaseRank } else { $_.ServicePackRank } };Descending=$true}, @{e='ProfileRank';Descending=$true}, @{e='VersionRank';Descending=$true}, @{e='DirRank';Descending=$true} $top = $ranked | Select-Object -First 1 if ($top) { # Add Family note property without mutating the base type $clone = $top.PSObject.Copy() $clone.PSObject.Properties.Remove('ReleaseRank') $clone.PSObject.Properties.Remove('ProfileRank') $clone.PSObject.Properties.Remove('ServicePackRank') $clone.PSObject.Properties.Remove('VersionRank') $clone.PSObject.Properties.Remove('DirRank') $clone | Add-Member -NotePropertyName Family -NotePropertyValue $fam -Force $clone } } [pscustomobject]@{ All = $all LatestByFamily = @($selected) } } function Get-DotNetFrameworkFamilyCapabilities { <# .SYNOPSIS One row per .NET Framework family with safe-array fields and a probed toolset. .DESCRIPTION Wraps Get-DotNetFrameworkLatestByFamily, keeps only rows with an existing InstallPath, and returns compact objects suitable for invoking toolchains. Guarantees arrays for: - TargetPacksApplicable - TargetPacksApplicableTFM - TargetPacksApplicableVersionString Adds: - ToolchainBitness : 'x64' or 'x86' (derived from RegistryView) - AvailableToolset : array of existing tools { Name, Path, Version, ProductVersion, LastWriteUtc, LengthBytes } (non-existing EXEs are NOT returned) .PARAMETER IncludeNonInstalled Pass-through to Get-DotNetFrameworkLatestByFamily. .PARAMETER IncludeFeatureState Pass-through to Get-DotNetFrameworkLatestByFamily. .PARAMETER ToolNames EXE basenames (without .exe) to probe under each InstallPath root. Only present tools are returned. .EXAMPLE Get-DotNetFrameworkFamilyCapabilities | Select-Object Family,ToolchainBitness,InstallPath, @{N='Tools';E={$_.AvailableToolset | ForEach-Object Name -join ','}} | Format-Table -AutoSize #> [CmdletBinding()] param( [switch]$IncludeNonInstalled, [switch]$IncludeFeatureState, [string[]]$ToolNames = @( 'csc','vbc','msbuild','ngen','ilasm','al','regasm','regsvcs','installutil', 'aspnet_regiis','aspnet_compiler','aspnet_regsql','aspnet_state','jsc','mscorsvw', 'resgen' # Will be omitted unless actually present under the Framework path ) ) # -- local helpers (kept PS5-safe) -- function To-StringArray { [Diagnostics.CodeAnalysis.SuppressMessage("PSUseApprovedVerbs","")] param([object]$Value) ,([string[]](@($Value) | Where-Object { $_ -ne $null })) } function To-BitnessLabel { [Diagnostics.CodeAnalysis.SuppressMessage("PSUseApprovedVerbs","")] param([string]$RegistryView) if ($RegistryView -eq '64-bit') { 'x64' } else { 'x86' } } function Probe-Exe { <# .SYNOPSIS Probe a single EXE via pure .NET FileInfo; returns $null if not found. .NOTES External reviewer note: returning $null for missing files simplifies filtering downstream. #> [Diagnostics.CodeAnalysis.SuppressMessage("PSUseApprovedVerbs","")] param( [Parameter(Mandatory)][string]$Directory, [Parameter(Mandatory)][string]$ExeBaseName ) $exePath = [System.IO.Path]::Combine($Directory, ('{0}.exe' -f $ExeBaseName)) try { $fi = [System.IO.FileInfo]::new($exePath) if (-not $fi.Exists) { return $null } $fvi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($fi.FullName) [pscustomobject]@{ Name = $ExeBaseName Path = $fi.FullName Version = $fvi.FileVersion ProductVersion = $fvi.ProductVersion LastWriteUtc = $fi.LastWriteTimeUtc LengthBytes = $fi.Length } } catch { # External reviewer note: swallow I/O errors into omission; use -Verbose for diagnostics if needed. Write-Verbose ("Probe-Exe: {0}" -f $_.Exception.Message) return $null } } function Get-ToolsForPath { [Diagnostics.CodeAnalysis.SuppressMessage("PSUseApprovedVerbs","")] param( [Parameter(Mandatory)][string]$InstallPath, [Parameter(Mandatory)][string[]]$Names ) ,(@( foreach ($n in $Names) { $tool = Probe-Exe -Directory $InstallPath -ExeBaseName $n if ($null -ne $tool) { $tool } } )) } # -- end helpers -- $result = if ($IncludeNonInstalled -or $IncludeFeatureState) { Get-DotNetFrameworkLatestByFamily -IncludeNonInstalled:$IncludeNonInstalled -IncludeFeatureState:$IncludeFeatureState } else { Get-DotNetFrameworkLatestByFamily } if (-not $result -or -not $result.LatestByFamily) { return @() } $result.LatestByFamily | Where-Object { $_.DirectoryExists -eq $true } | ForEach-Object { [pscustomobject]@{ Family = $_.Family ToolchainBitness = To-BitnessLabel $_.RegistryView TargetPacksApplicable = To-StringArray $_.TargetPacksApplicable TargetPacksApplicableTFM = To-StringArray $_.TargetPacksApplicableTFM TargetPacksApplicableVersionString = To-StringArray $_.TargetPacksApplicableVersionString InstallPath = $_.InstallPath AvailableToolset = Get-ToolsForPath -InstallPath $_.InstallPath -Names $ToolNames } } } #Get-DotNetFrameworkFamilyCapabilities | ConvertTo-Json |