NetscootShared/Dotnet/DotnetPath.ps1

# The dotnet executable netscoot runs: the stored path when one is set, otherwise the one on PATH.

function Get-NetscootSettingFile {
    [CmdletBinding()]
    [OutputType([string])]
    param()
    return [System.IO.Path]::Combine((Get-MoveJournalAppDataRoot), 'netscoot', 'settings.json')
}

function Get-StoredDotnetPath {
    # Returns $null when no path is stored.
    [CmdletBinding()]
    [OutputType([string])]
    param()
    $file = Get-NetscootSettingFile
    if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { return $null }
    $settings = [System.IO.File]::ReadAllText($file) | ConvertFrom-Json
    $stored = $settings.PSObject.Properties['dotnetPath']
    if (-not $stored) { return $null }
    return [string]$stored.Value
}

function Save-StoredDotnetPath {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Path)
    $file = Get-NetscootSettingFile
    $folder = Split-Path -Parent $file
    if (-not (Test-Path -LiteralPath $folder)) { New-Item -ItemType Directory -Path $folder -Force | Out-Null }
    $json = [pscustomobject]@{ dotnetPath = $Path } | ConvertTo-Json
    [System.IO.File]::WriteAllText($file, $json, [System.Text.UTF8Encoding]::new($false))
}

function Remove-StoredDotnetPath {
    [CmdletBinding()]
    param()
    $file = Get-NetscootSettingFile
    if (Test-Path -LiteralPath $file -PathType Leaf) { Remove-Item -LiteralPath $file -Force }
}

function Get-ExternalToolVersion {
    # First line of the tool's version output, or $null when the probe fails (a dotnet host with no SDK).
    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][string]$Path)
    # Ignore keeps Windows PowerShell 5.1 from recording the probe's stderr as an error, and the exit code decides.
    $prev = $ErrorActionPreference
    $ErrorActionPreference = 'Ignore'
    try { $out = @(& $Path --version 2>$null) }
    finally { $ErrorActionPreference = $prev }
    if ($LASTEXITCODE -ne 0) { return $null }
    return "$($out | Select-Object -First 1)".Trim()
}

function Resolve-DotnetCommand {
    # Returns { Path; Source } with Source 'Stored' or 'Path', or $null when neither gives a dotnet.
    [CmdletBinding()]
    param()
    $stored = Get-StoredDotnetPath
    if ($stored) {
        if (-not (Test-Path -LiteralPath $stored -PathType Leaf)) {
            throw "The stored dotnet path '$stored' does not exist. Store the one to use with Set-NetscootDotnetPath, or remove the stored path with Clear-NetscootDotnetPath."
        }
        return [pscustomobject]@{ Path = $stored; Source = 'Stored' }
    }
    $onPath = Get-Command dotnet -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($onPath) { return [pscustomobject]@{ Path = $onPath.Source; Source = 'Path' } }
    return $null
}

function Find-DotnetInstall {
    # The .NET SDK installs on this machine, read from where an install records or documents itself, newest first.
    [CmdletBinding()]
    param()
    $onWindows = Test-IsWindowsHost
    $candidates = [System.Collections.Generic.List[object]]::new()

    foreach ($name in 'DOTNET_ROOT', 'DOTNET_ROOT(x86)') {
        $value = [Environment]::GetEnvironmentVariable($name)
        if ($value) { $candidates.Add([pscustomobject]@{ Folder = $value; FoundIn = $name }) }
    }

    if ($onWindows) {
        foreach ($key in 'HKLM:\SOFTWARE\dotnet\Setup\InstalledVersions', 'HKLM:\SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions') {
            if (-not (Test-Path -LiteralPath $key)) { continue }
            foreach ($arch in (Get-ChildItem -LiteralPath $key)) {
                $location = $arch.GetValue('InstallLocation')
                if ($location) { $candidates.Add([pscustomobject]@{ Folder = [string]$location; FoundIn = 'Registry' }) }
            }
        }
        foreach ($folder in @(
                $(if ($env:ProgramFiles) { Join-Path $env:ProgramFiles 'dotnet' }),
                $(if (${env:ProgramFiles(x86)}) { Join-Path ${env:ProgramFiles(x86)} 'dotnet' }),
                $(if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA (Join-Path 'Microsoft' 'dotnet') }))) {
            if ($folder) { $candidates.Add([pscustomobject]@{ Folder = $folder; FoundIn = 'DefaultFolder' }) }
        }
    } else {
        if (Test-Path -LiteralPath '/etc/dotnet' -PathType Container) {
            foreach ($record in (Get-ChildItem -LiteralPath '/etc/dotnet' -Filter 'install_location*' -File)) {
                $location = Get-Content -LiteralPath $record.FullName -TotalCount 1
                if ($location) { $candidates.Add([pscustomobject]@{ Folder = "$location".Trim(); FoundIn = $record.Name }) }
            }
        }
        foreach ($folder in '/usr/local/share/dotnet', '/usr/share/dotnet', '/usr/lib/dotnet', '/usr/lib64/dotnet', (Join-Path $HOME '.dotnet')) {
            $candidates.Add([pscustomobject]@{ Folder = $folder; FoundIn = 'DefaultFolder' })
        }
    }

    $leaf = if ($onWindows) { 'dotnet.exe' } else { 'dotnet' }
    $seen = [System.Collections.Generic.List[string]]::new()
    $installs = foreach ($candidate in $candidates) {
        $exe = [System.IO.Path]::GetFullPath((Join-Path $candidate.Folder $leaf))
        if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { continue }
        if (Test-PathInList -Path $exe -List $seen) { continue }
        $seen.Add($exe)
        $version = Get-ExternalToolVersion -Path $exe
        if (-not $version) { continue }
        [pscustomobject]@{ PSTypeName = 'Netscoot.DotnetInstall'; Version = $version; Path = $exe; FoundIn = $candidate.FoundIn }
    }
    @($installs) | Sort-Object -Descending -Property { [version]($_.Version -replace '-.*$', '') }
}

function Test-InteractiveSession {
    # False when nobody can answer a prompt: a service, a redirected input, or a host started non-interactive.
    [CmdletBinding()]
    [OutputType([bool])]
    param()
    if (-not [Environment]::UserInteractive) { return $false }
    if ([Console]::IsInputRedirected) { return $false }
    foreach ($argument in [Environment]::GetCommandLineArgs()) {
        if ($argument -match '^[-/]noni') { return $false }
    }
    return $true
}

function Read-DotnetInstallChoice {
    # Asks which install to use. Returns the chosen install, or $null when the user declines.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][System.Management.Automation.PSCmdlet]$Cmdlet,
        [Parameter(Mandatory)][object[]]$Installs
    )
    $afterwards = "netscoot stores the path you choose and uses it for every later command.`n" +
        "Set-NetscootDotnetPath changes it, and Clear-NetscootDotnetPath removes it."
    $choices = [System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription]]::new()

    if ($Installs.Count -eq 1) {
        $only = $Installs[0]
        $message = "Moving a .NET project needs the .NET SDK. netscoot found one install:`n`n" +
            " .NET SDK $($only.Version) $($only.Path)`n`n" +
            "$afterwards`n`nUse this install?"
        $choices.Add([System.Management.Automation.Host.ChoiceDescription]::new('&Yes', 'Use this install and store its path.'))
        $choices.Add([System.Management.Automation.Host.ChoiceDescription]::new('&No', 'Stop the move and store nothing.'))
        $answer = $Cmdlet.Host.UI.PromptForChoice('dotnet is not on PATH', $message, $choices, 0)
        if ($answer -eq 0) { return $only }
        return $null
    }

    $lines = for ($i = 0; $i -lt $Installs.Count; $i++) {
        "[$($i + 1)] .NET SDK $($Installs[$i].Version) $($Installs[$i].Path)"
        $choices.Add([System.Management.Automation.Host.ChoiceDescription]::new("&$($i + 1)", $Installs[$i].Path))
    }
    $choices.Add([System.Management.Automation.Host.ChoiceDescription]::new('&None of these', 'Stop the move and store nothing.'))
    $message = "Moving a .NET project needs the .NET SDK. netscoot found these installs:`n`n" +
        "$($lines -join "`n")`n`n" +
        "$afterwards`n`nWhich install should netscoot use?"
    $answer = $Cmdlet.Host.UI.PromptForChoice('dotnet is not on PATH', $message, $choices, -1)
    if ($answer -ge 0 -and $answer -lt $Installs.Count) { return $Installs[$answer] }
    return $null
}