Private/Logic/Eigenverft.Manifested.Sandbox.Runtime.Python.Discovery.ps1

<#
    Eigenverft.Manifested.Sandbox.Runtime.Python.Discovery
#>


function ConvertTo-PythonVersion {
<#
.SYNOPSIS
Normalizes Python version text into a comparable version object.
 
.DESCRIPTION
Extracts the first `major.minor.patch` fragment from arbitrary Python version
text and converts it into a `[version]` instance so runtime release checks can
compare versions consistently.
 
.PARAMETER VersionText
Raw version text emitted by Python tooling or release metadata.
 
.EXAMPLE
ConvertTo-PythonVersion -VersionText 'Python 3.13.2'
 
.EXAMPLE
ConvertTo-PythonVersion -VersionText '3.13.2rc1'
 
.NOTES
Returns `$null` when no semantic version fragment can be found.
#>

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

    if ([string]::IsNullOrWhiteSpace($VersionText)) {
        return $null
    }

    $match = [regex]::Match($VersionText, '(\d+\.\d+\.\d+)')
    if (-not $match.Success) {
        return $null
    }

    return [version]$match.Groups[1].Value
}

function Get-PythonManagedBaselineVersion {
    [CmdletBinding()]
    param()

    return [version]'3.13.0'
}

function Test-PythonManagedReleaseVersion {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [version]$Version
    )

    $baseline = Get-PythonManagedBaselineVersion
    return ($Version.Major -eq $baseline.Major) -and ($Version.Minor -eq $baseline.Minor) -and ($Version -ge $baseline)
}

function Test-PythonExternalRuntimeVersion {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [version]$Version
    )

    $baseline = Get-PythonManagedBaselineVersion
    return ($Version.Major -gt $baseline.Major) -or (($Version.Major -eq $baseline.Major) -and ($Version.Minor -ge $baseline.Minor))
}

function Get-PythonFlavor {
    [CmdletBinding()]
    param()

    if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
        throw 'Only Windows hosts are supported by this Python runtime bootstrap.'
    }

    $archHints = @($env:PROCESSOR_ARCHITECTURE, $env:PROCESSOR_ARCHITEW6432) -join ';'
    if ($archHints -match 'ARM64') {
        return 'arm64'
    }

    if ([Environment]::Is64BitOperatingSystem) {
        return 'amd64'
    }

    throw 'Only 64-bit Windows targets are supported by this Python runtime bootstrap.'
}

function Get-ManagedPythonRuntimeHome {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Version,

        [Parameter(Mandatory = $true)]
        [string]$Flavor,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    return (Join-Path $layout.PythonToolsRoot ($Version + '\' + $Flavor))
}

function Get-PythonRuntimePthPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PythonHome
    )

    if (-not (Test-Path -LiteralPath $PythonHome)) {
        return $null
    }

    $pthFile = @(Get-ChildItem -LiteralPath $PythonHome -File -Filter 'python*._pth' -ErrorAction SilentlyContinue | Sort-Object -Property Name | Select-Object -First 1)
    if (-not $pthFile) {
        return $null
    }

    return $pthFile[0].FullName
}

function Get-InstalledPythonRuntime {
    [CmdletBinding()]
    param(
        [string]$Flavor,
        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    if ([string]::IsNullOrWhiteSpace($Flavor)) {
        $Flavor = Get-PythonFlavor
    }

    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    $entries = @()

    if (Test-Path -LiteralPath $layout.PythonToolsRoot) {
        $versionRoots = Get-ChildItem -LiteralPath $layout.PythonToolsRoot -Directory -ErrorAction SilentlyContinue |
            Sort-Object -Descending -Property @{ Expression = { ConvertTo-PythonVersion -VersionText $_.Name } }

        foreach ($versionRoot in $versionRoots) {
            $pythonHome = Join-Path $versionRoot.FullName $Flavor
            if (-not (Test-Path -LiteralPath $pythonHome)) {
                continue
            }

            $validation = Test-PythonRuntime -PythonHome $pythonHome -LocalRoot $layout.LocalRoot
            $expectedVersion = ConvertTo-PythonVersion -VersionText $versionRoot.Name
            $reportedVersion = ConvertTo-PythonVersion -VersionText $validation.ReportedVersion
            $versionMatches = (-not $reportedVersion) -or (-not $expectedVersion) -or ($reportedVersion -eq $expectedVersion)

            $entries += [pscustomobject]@{
                Version        = $versionRoot.Name
                Flavor         = $Flavor
                PythonHome     = $pythonHome
                PythonExe      = $validation.PythonExe
                Validation     = $validation
                VersionMatches = $versionMatches
                PipVersion     = $validation.PipVersion
                IsReady        = ($validation.IsReady -and $versionMatches)
            }
        }
    }

    [pscustomobject]@{
        Current = ($entries | Where-Object { $_.IsReady } | Select-Object -First 1)
        Valid   = @($entries | Where-Object { $_.IsReady })
        Invalid = @($entries | Where-Object { -not $_.IsReady })
    }
}

function Get-ManifestedPythonExternalPaths {
    [CmdletBinding()]
    param(
        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    $candidatePaths = New-Object System.Collections.Generic.List[string]

    foreach ($commandName in @('python.exe', 'python')) {
        foreach ($command in @(Get-Command -Name $commandName -CommandType Application -All -ErrorAction SilentlyContinue)) {
            $commandPath = $null
            if ($command.PSObject.Properties['Path'] -and $command.Path) {
                $commandPath = $command.Path
            }
            elseif ($command.PSObject.Properties['Source'] -and $command.Source) {
                $commandPath = $command.Source
            }

            if (-not [string]::IsNullOrWhiteSpace($commandPath) -and $commandPath -like '*.exe') {
                $candidatePaths.Add($commandPath) | Out-Null
            }
        }
    }

    $additionalPatterns = @()
    if (-not [string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
        $additionalPatterns += (Join-Path $env:LOCALAPPDATA 'Programs\Python\Python*\python.exe')
    }
    if (-not [string]::IsNullOrWhiteSpace($env:ProgramFiles)) {
        $additionalPatterns += (Join-Path $env:ProgramFiles 'Python*\python.exe')
    }
    if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) {
        $additionalPatterns += (Join-Path $env:USERPROFILE '.pyenv\pyenv-win\versions\*\python.exe')
    }

    foreach ($pattern in $additionalPatterns) {
        foreach ($candidate in @(Get-ChildItem -Path $pattern -File -ErrorAction SilentlyContinue)) {
            $candidatePaths.Add($candidate.FullName) | Out-Null
        }
    }

    $resolvedPaths = New-Object System.Collections.Generic.List[string]
    foreach ($candidatePath in @($candidatePaths | Select-Object -Unique)) {
        $fullCandidatePath = Get-ManifestedFullPath -Path $candidatePath
        if ([string]::IsNullOrWhiteSpace($fullCandidatePath) -or -not (Test-Path -LiteralPath $fullCandidatePath)) {
            continue
        }
        if (Test-ManifestedPathIsUnderRoot -Path $fullCandidatePath -RootPath $layout.PythonToolsRoot) {
            continue
        }
        if ($fullCandidatePath -like '*\WindowsApps\python.exe') {
            continue
        }

        $resolvedPaths.Add($fullCandidatePath) | Out-Null
    }

    return @($resolvedPaths | Select-Object -Unique)
}

function Test-ExternalPythonRuntime {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PythonExe,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $versionProbe = Get-PythonReportedVersionProbe -PythonExe $PythonExe -LocalRoot $LocalRoot
    $reportedVersion = $versionProbe.ReportedVersion
    $versionObject = ConvertTo-PythonVersion -VersionText $reportedVersion
    $pipProbe = if ($versionObject -and (Test-PythonExternalRuntimeVersion -Version $versionObject)) { Get-PythonPipVersionProbe -PythonExe $PythonExe -LocalRoot $LocalRoot } else { $null }
    $pipVersion = if ($pipProbe) { $pipProbe.PipVersion } else { $null }
    $isReady = ($versionObject -and (Test-PythonExternalRuntimeVersion -Version $versionObject) -and -not [string]::IsNullOrWhiteSpace($pipVersion))

    [pscustomobject]@{
        Status          = if ($isReady) { 'Ready' } else { 'Invalid' }
        IsReady         = $isReady
        PythonHome      = if (Test-Path -LiteralPath $PythonExe) { Split-Path -Parent $PythonExe } else { $null }
        PythonExe       = $PythonExe
        ReportedVersion = if ($versionObject) { $versionObject.ToString() } else { $reportedVersion }
        PipVersion      = $pipVersion
        VersionCommandResult = $versionProbe.CommandResult
        PipCommandResult = if ($pipProbe) { $pipProbe.CommandResult } else { $null }
    }
}

function Get-ManifestedPythonRuntimeFromCandidatePath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$CandidatePath,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $validation = Test-ExternalPythonRuntime -PythonExe $CandidatePath -LocalRoot $LocalRoot
    if (-not $validation.IsReady) {
        return $null
    }

    [pscustomobject]@{
        Version     = $validation.ReportedVersion
        Flavor      = $null
        PythonHome  = $validation.PythonHome
        PythonExe   = $validation.PythonExe
        Validation  = $validation
        PipVersion  = $validation.PipVersion
        IsReady     = $true
        Source      = 'External'
        Discovery   = 'Path'
    }
}

function Get-SystemPythonRuntime {
    [CmdletBinding()]
    param(
        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    foreach ($candidatePath in @(Get-ManifestedPythonExternalPaths -LocalRoot $LocalRoot)) {
        $runtime = Get-ManifestedPythonRuntimeFromCandidatePath -CandidatePath $candidatePath -LocalRoot $LocalRoot
        if ($runtime) {
            return $runtime
        }
    }

    return $null
}

function Get-PythonRuntimeState {
    [CmdletBinding()]
    param(
        [string]$Flavor,
        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    try {
        if ([string]::IsNullOrWhiteSpace($Flavor)) {
            $Flavor = Get-PythonFlavor
        }

        $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    }
    catch {
        return [pscustomobject]@{
            Status              = 'Blocked'
            LocalRoot           = $LocalRoot
            Layout              = $null
            Flavor              = $Flavor
            CurrentVersion      = $null
            RuntimeHome         = $null
            RuntimeSource       = $null
            ExecutablePath      = $null
            Runtime             = $null
            PipVersion          = $null
            InvalidRuntimeHomes = @()
            Package             = $null
            PackagePath         = $null
            PartialPaths        = @()
            BlockedReason       = $_.Exception.Message
        }
    }

    $partialPaths = @()
    if (Test-Path -LiteralPath $layout.PythonCacheRoot) {
        $partialPaths += @(Get-ChildItem -LiteralPath $layout.PythonCacheRoot -File -Filter '*.download' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName)
    }
    $partialPaths += @(Get-ManifestedStageDirectories -Prefix 'python' -Mode TemporaryShort -LegacyRootPaths @($layout.ToolsRoot) | Select-Object -ExpandProperty FullName)

    $installed = Get-InstalledPythonRuntime -Flavor $Flavor -LocalRoot $layout.LocalRoot
    $managedRuntime = $installed.Current
    $externalRuntime = $null
    if (-not $managedRuntime) {
        $externalRuntime = Get-SystemPythonRuntime -LocalRoot $layout.LocalRoot
    }

    $currentRuntime = if ($managedRuntime) { $managedRuntime } else { $externalRuntime }
    $runtimeSource = if ($managedRuntime) { 'Managed' } elseif ($externalRuntime) { 'External' } else { $null }
    $invalidRuntimeHomes = @($installed.Invalid | Select-Object -ExpandProperty PythonHome)
    $package = Get-LatestCachedPythonRuntimePackage -Flavor $Flavor -LocalRoot $layout.LocalRoot

    if ($invalidRuntimeHomes.Count -gt 0) {
        $status = 'NeedsRepair'
    }
    elseif ($partialPaths.Count -gt 0) {
        $status = 'Partial'
    }
    elseif ($currentRuntime) {
        $status = 'Ready'
    }
    elseif ($package) {
        $status = 'NeedsInstall'
    }
    else {
        $status = 'Missing'
    }

    [pscustomobject]@{
        Status              = $status
        LocalRoot           = $layout.LocalRoot
        Layout              = $layout
        Flavor              = $Flavor
        CurrentVersion      = if ($currentRuntime) { $currentRuntime.Version } elseif ($package) { $package.Version } else { $null }
        RuntimeHome         = if ($currentRuntime) { $currentRuntime.PythonHome } else { $null }
        RuntimeSource       = $runtimeSource
        ExecutablePath      = if ($currentRuntime) { $currentRuntime.PythonExe } else { $null }
        Runtime             = if ($currentRuntime) { $currentRuntime.Validation } else { $null }
        PipVersion          = if ($currentRuntime -and $currentRuntime.PSObject.Properties['PipVersion']) { $currentRuntime.PipVersion } else { $null }
        InvalidRuntimeHomes = $invalidRuntimeHomes
        Package             = $package
        PackagePath         = if ($package) { $package.Path } else { $null }
        PartialPaths        = $partialPaths
        BlockedReason       = $null
    }
}