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

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


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

    $pthPath = Get-PythonRuntimePthPath -PythonHome $PythonHome
    if ([string]::IsNullOrWhiteSpace($pthPath) -or -not (Test-Path -LiteralPath $pthPath)) {
        return [pscustomobject]@{
            Exists                 = $false
            PthPath                = $pthPath
            ImportSiteEnabled      = $false
            SitePackagesPathListed = $false
            IsReady                = $false
        }
    }

    $lines = @(Get-Content -LiteralPath $pthPath -ErrorAction SilentlyContinue)
    $importSiteEnabled = $false
    $sitePackagesPathListed = $false

    foreach ($line in $lines) {
        $trimmedLine = $line.Trim()
        if ($trimmedLine -eq 'import site') {
            $importSiteEnabled = $true
        }
        elseif ($trimmedLine -ieq 'Lib\site-packages') {
            $sitePackagesPathListed = $true
        }
    }

    [pscustomobject]@{
        Exists                 = $true
        PthPath                = $pthPath
        ImportSiteEnabled      = $importSiteEnabled
        SitePackagesPathListed = $sitePackagesPathListed
        IsReady                = ($importSiteEnabled -and $sitePackagesPathListed)
    }
}

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

    $pthState = Test-PythonSiteImportsEnabled -PythonHome $PythonHome
    if (-not $pthState.Exists) {
        throw "Could not find the Python runtime ._pth file under $PythonHome."
    }

    $sitePackagesRoot = Join-Path $PythonHome 'Lib\site-packages'
    New-ManifestedDirectory -Path $sitePackagesRoot | Out-Null

    $lines = @(Get-Content -LiteralPath $pthState.PthPath -ErrorAction Stop)
    $updatedLines = New-Object System.Collections.Generic.List[string]
    $hasImportSite = $false
    $hasSitePackages = $false

    foreach ($line in $lines) {
        $trimmedLine = $line.Trim()

        if ($trimmedLine -match '^(#\s*)?import\s+site$') {
            if (-not $hasImportSite) {
                $updatedLines.Add('import site') | Out-Null
                $hasImportSite = $true
            }

            continue
        }

        if ($trimmedLine -ieq 'Lib\site-packages') {
            if (-not $hasSitePackages) {
                $updatedLines.Add('Lib\site-packages') | Out-Null
                $hasSitePackages = $true
            }

            continue
        }

        $updatedLines.Add($line) | Out-Null
    }

    if (-not $hasSitePackages) {
        $updatedLines.Add('Lib\site-packages') | Out-Null
    }
    if (-not $hasImportSite) {
        $updatedLines.Add('import site') | Out-Null
    }

    Set-Content -LiteralPath $pthState.PthPath -Value @($updatedLines) -Encoding ASCII
    return (Test-PythonSiteImportsEnabled -PythonHome $PythonHome)
}

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

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

    $probe = Get-PythonReportedVersionProbe -PythonExe $PythonExe -LocalRoot $LocalRoot
    return $probe.ReportedVersion
}

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

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

    if (-not (Test-Path -LiteralPath $PythonExe)) {
        return [pscustomobject]@{
            ReportedVersion = $null
            CommandResult   = $null
        }
    }

    $commandResult = Invoke-ManifestedPythonCommand -PythonExe $PythonExe -Arguments @('-c', 'import sys; print(*sys.version_info[:3], sep=chr(46))') -LocalRoot $LocalRoot
    $reportedVersion = $null
    if ($commandResult.ExitCode -eq 0) {
        $versionLine = @($commandResult.OutputLines | Select-Object -First 1)
        if ($versionLine) {
            $reportedVersion = $versionLine[0].ToString().Trim()
        }
    }

    return [pscustomobject]@{
        ReportedVersion = $reportedVersion
        CommandResult   = $commandResult
    }
}

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

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

    $probe = Get-PythonPipVersionProbe -PythonExe $PythonExe -LocalRoot $LocalRoot
    return $probe.PipVersion
}

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

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

    if (-not (Test-Path -LiteralPath $PythonExe)) {
        return [pscustomobject]@{
            PipVersion    = $null
            CommandResult = $null
        }
    }

    $commandResult = Invoke-ManifestedPipAwarePythonCommand -PythonExe $PythonExe -Arguments @('-m', 'pip', '--version') -LocalRoot $LocalRoot
    $pipVersion = $null
    if ($commandResult.ExitCode -eq 0) {
        $versionLine = @($commandResult.OutputLines | Select-Object -First 1)
        if ($versionLine) {
            $pipVersion = $versionLine[0].ToString().Trim()
        }
    }

    return [pscustomobject]@{
        PipVersion    = $pipVersion
        CommandResult = $commandResult
    }
}

function Get-PythonCommandFailureHint {
    [CmdletBinding()]
    param(
        [pscustomobject]$CommandResult,

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

    if (-not $CommandResult) {
        return $null
    }

    $combinedText = @(
        $CommandResult.ExceptionMessage
        $CommandResult.OutputText
    ) -join [Environment]::NewLine

    if ($combinedText -match 'No module named encodings|init_fs_encoding') {
        return 'The Python process started with an invalid import-path configuration. The managed runtime now clears PYTHONHOME and PYTHONPATH automatically; if this persists, repair the managed runtime cache and retry.'
    }

    return $null
}

function New-PythonRuntimeValidationFailureMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Operation,

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

        [string]$ExpectedVersion,

        [string]$ReportedVersion,

        [pscustomobject]$CommandResult,

        [pscustomobject]$SiteImportsState,

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

    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add(("Python runtime validation failed during {0} at {1}." -f $Operation, $PythonHome)) | Out-Null

    if (-not [string]::IsNullOrWhiteSpace($ExpectedVersion)) {
        $lines.Add(("Expected version: {0}." -f $ExpectedVersion)) | Out-Null
    }
    if (-not [string]::IsNullOrWhiteSpace($ReportedVersion)) {
        $lines.Add(("Reported version: {0}." -f $ReportedVersion)) | Out-Null
    }
    if ($CommandResult -and $null -ne $CommandResult.ExitCode) {
        $lines.Add(("python.exe exit code: {0}." -f $CommandResult.ExitCode)) | Out-Null
    }
    if ($CommandResult -and -not [string]::IsNullOrWhiteSpace($CommandResult.ExceptionMessage)) {
        $lines.Add(("Startup error: {0}" -f $CommandResult.ExceptionMessage)) | Out-Null
    }
    if ($CommandResult -and -not [string]::IsNullOrWhiteSpace($CommandResult.OutputText)) {
        $lines.Add(("python.exe output:{0}{1}" -f [Environment]::NewLine, $CommandResult.OutputText)) | Out-Null
    }
    if ($SiteImportsState) {
        $lines.Add(("Site imports: import site={0}; Lib\\site-packages listed={1}; pth={2}." -f $SiteImportsState.ImportSiteEnabled, $SiteImportsState.SitePackagesPathListed, $SiteImportsState.PthPath)) | Out-Null
    }
    if ($CommandResult -and $CommandResult.IsManagedPython -and @($CommandResult.SanitizedVariables).Count -gt 0) {
        $lines.Add(("Managed runtime startup cleared: {0}." -f (@($CommandResult.SanitizedVariables) -join ', '))) | Out-Null
    }

    $hint = Get-PythonCommandFailureHint -CommandResult $CommandResult -LocalRoot $LocalRoot
    if (-not [string]::IsNullOrWhiteSpace($hint)) {
        $lines.Add($hint) | Out-Null
    }

    return (@($lines) -join [Environment]::NewLine)
}

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

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

    $pythonExe = Join-Path $PythonHome 'python.exe'
    $pipCmd = Join-Path $PythonHome 'pip.cmd'
    $pip3Cmd = Join-Path $PythonHome 'pip3.cmd'
    $siteState = Test-PythonSiteImportsEnabled -PythonHome $PythonHome
    $versionCommandResult = $null
    $pipCommandResult = $null

    if (-not (Test-Path -LiteralPath $PythonHome)) {
        $status = 'Missing'
        $reportedVersion = $null
        $pipVersion = $null
    }
    elseif (-not (Test-Path -LiteralPath $pythonExe)) {
        $status = 'NeedsRepair'
        $reportedVersion = $null
        $pipVersion = $null
        $versionCommandResult = $null
        $pipCommandResult = $null
    }
    else {
        $versionProbe = Get-PythonReportedVersionProbe -PythonExe $pythonExe -LocalRoot $LocalRoot
        $reportedVersion = $versionProbe.ReportedVersion
        $versionCommandResult = $versionProbe.CommandResult
        $versionObject = ConvertTo-PythonVersion -VersionText $reportedVersion
        $pipProbe = if ($siteState.ImportSiteEnabled) { Get-PythonPipVersionProbe -PythonExe $pythonExe -LocalRoot $LocalRoot } else { $null }
        $pipVersion = if ($pipProbe) { $pipProbe.PipVersion } else { $null }
        $pipCommandResult = if ($pipProbe) { $pipProbe.CommandResult } else { $null }
        $hasWrappers = (Test-Path -LiteralPath $pipCmd) -and (Test-Path -LiteralPath $pip3Cmd)
        $status = if ($versionObject -and $siteState.IsReady -and $hasWrappers -and -not [string]::IsNullOrWhiteSpace($pipVersion)) { 'Ready' } else { 'NeedsRepair' }
    }

    [pscustomobject]@{
        Status            = $status
        IsReady           = ($status -eq 'Ready')
        PythonHome        = $PythonHome
        PythonExe         = $pythonExe
        PipCmd            = $pipCmd
        Pip3Cmd           = $pip3Cmd
        ReportedVersion   = $reportedVersion
        PipVersion        = $pipVersion
        PthPath           = $siteState.PthPath
        SiteImports       = $siteState
        VersionCommandResult = $versionCommandResult
        PipCommandResult  = $pipCommandResult
        ValidationHint    = if (-not [string]::IsNullOrWhiteSpace($reportedVersion) -and -not [string]::IsNullOrWhiteSpace($pipVersion)) {
            $null
        }
        elseif ($versionCommandResult -and [string]::IsNullOrWhiteSpace($reportedVersion)) {
            Get-PythonCommandFailureHint -CommandResult $versionCommandResult -LocalRoot $LocalRoot
        }
        elseif ($pipCommandResult -and [string]::IsNullOrWhiteSpace($pipVersion)) {
            Get-PythonCommandFailureHint -CommandResult $pipCommandResult -LocalRoot $LocalRoot
        }
        else {
            $null
        }
    }
}

function Test-PythonRuntimeFromState {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$State,

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

    if (-not $State.RuntimeHome -and -not $State.ExecutablePath) {
        return $null
    }

    if ($State.RuntimeSource -eq 'Managed' -and $State.RuntimeHome) {
        return (Test-PythonRuntime -PythonHome $State.RuntimeHome -LocalRoot $LocalRoot)
    }

    if ($State.RuntimeSource -eq 'External' -and $State.ExecutablePath) {
        return (Test-ExternalPythonRuntime -PythonExe $State.ExecutablePath -LocalRoot $LocalRoot)
    }

    return $null
}