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

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


function Repair-PythonRuntime {
    [CmdletBinding()]
    param(
        [pscustomobject]$State,
        [string[]]$CorruptPackagePaths = @(),
        [string]$Flavor,
        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    if (-not $State) {
        $State = Get-PythonRuntimeState -Flavor $Flavor -LocalRoot $LocalRoot
    }

    $pathsToRemove = New-Object System.Collections.Generic.List[string]
    foreach ($path in @($State.PartialPaths)) {
        if (-not [string]::IsNullOrWhiteSpace($path)) {
            $pathsToRemove.Add($path) | Out-Null
        }
    }
    foreach ($path in @($State.InvalidRuntimeHomes)) {
        if (-not [string]::IsNullOrWhiteSpace($path)) {
            $pathsToRemove.Add($path) | Out-Null
        }
    }
    foreach ($path in @($CorruptPackagePaths)) {
        if (-not [string]::IsNullOrWhiteSpace($path)) {
            $pathsToRemove.Add($path) | Out-Null
        }
    }

    $removedPaths = New-Object System.Collections.Generic.List[string]
    foreach ($path in ($pathsToRemove | Select-Object -Unique)) {
        if (Remove-ManifestedPath -Path $path) {
            $removedPaths.Add($path) | Out-Null
        }
    }

    [pscustomobject]@{
        Action       = if ($removedPaths.Count -gt 0) { 'Repaired' } else { 'Skipped' }
        RemovedPaths = @($removedPaths)
        LocalRoot    = $State.LocalRoot
        Layout       = $State.Layout
    }
}

function Install-PythonRuntime {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$PackageInfo,

        [string]$Flavor,
        [string]$LocalRoot = (Get-ManifestedLocalRoot),
        [switch]$ForceInstall
    )

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

    $pythonHome = Get-ManagedPythonRuntimeHome -Version $PackageInfo.Version -Flavor $Flavor -LocalRoot $LocalRoot
    $currentValidation = Test-PythonRuntime -PythonHome $pythonHome -LocalRoot $LocalRoot

    if ($ForceInstall -or $currentValidation.Status -ne 'Ready') {
        New-ManifestedDirectory -Path (Split-Path -Parent $pythonHome) | Out-Null

        $stageInfo = $null
        try {
            $stageInfo = Expand-ManifestedArchiveToStage -PackagePath $PackageInfo.Path -Prefix 'python'
            if (-not (Test-Path -LiteralPath $stageInfo.ExpandedRoot)) {
                throw 'The Python embeddable ZIP did not extract as expected.'
            }

            if (Test-Path -LiteralPath $pythonHome) {
                Remove-Item -LiteralPath $pythonHome -Recurse -Force
            }

            New-ManifestedDirectory -Path $pythonHome | Out-Null
            Get-ChildItem -LiteralPath $stageInfo.ExpandedRoot -Force | ForEach-Object {
                Move-Item -LiteralPath $_.FullName -Destination $pythonHome -Force
            }

            $siteState = Enable-PythonSiteImports -PythonHome $pythonHome
            if (-not $siteState.IsReady) {
                throw "Python site import enablement failed for $pythonHome."
            }
        }
        finally {
            if ($stageInfo) {
                Remove-ManifestedPath -Path $stageInfo.StagePath | Out-Null
            }
        }
    }

    $pythonExe = Join-Path $pythonHome 'python.exe'
    $versionProbe = Get-PythonReportedVersionProbe -PythonExe $pythonExe -LocalRoot $LocalRoot
    $reportedVersion = $versionProbe.ReportedVersion
    $reportedVersionObject = ConvertTo-PythonVersion -VersionText $reportedVersion
    $expectedVersionObject = ConvertTo-PythonVersion -VersionText $PackageInfo.Version
    if (-not $reportedVersionObject -or -not $expectedVersionObject -or $reportedVersionObject -ne $expectedVersionObject) {
        throw (New-PythonRuntimeValidationFailureMessage -Operation 'post-install version check' -PythonHome $pythonHome -ExpectedVersion $PackageInfo.Version -ReportedVersion $reportedVersion -CommandResult $versionProbe.CommandResult -SiteImportsState $siteState -LocalRoot $LocalRoot)
    }

    $pipResult = Ensure-PythonPip -PythonExe $pythonExe -PythonHome $pythonHome -LocalRoot $LocalRoot
    $validation = Test-PythonRuntime -PythonHome $pythonHome -LocalRoot $LocalRoot
    if ($validation.Status -ne 'Ready') {
        $validationCommandResult = if ([string]::IsNullOrWhiteSpace($validation.ReportedVersion)) {
            $validation.VersionCommandResult
        }
        elseif ([string]::IsNullOrWhiteSpace($validation.PipVersion)) {
            $validation.PipCommandResult
        }
        else {
            $validation.VersionCommandResult
        }

        throw (New-PythonRuntimeValidationFailureMessage -Operation 'post-pip validation' -PythonHome $pythonHome -ExpectedVersion $PackageInfo.Version -ReportedVersion $validation.ReportedVersion -CommandResult $validationCommandResult -SiteImportsState $validation.SiteImports -LocalRoot $LocalRoot)
    }

    [pscustomobject]@{
        Action                = if ($ForceInstall -or $currentValidation.Status -ne 'Ready') { 'Installed' } else { 'Skipped' }
        Version               = $PackageInfo.Version
        Flavor                = $Flavor
        PythonHome            = $pythonHome
        PythonExe             = $validation.PythonExe
        PipCmd                = $validation.PipCmd
        Pip3Cmd               = $validation.Pip3Cmd
        PthPath               = $validation.PthPath
        PipVersion            = $validation.PipVersion
        PipResult             = $pipResult
        Source                = $PackageInfo.Source
    }
}