Private/Common/Eigenverft.Manifested.Sandbox.Shared.Pip.ps1

<#
    Eigenverft.Manifested.Sandbox.Shared.Pip
#>


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

    return (Join-Path $PythonHome 'pip.ini')
}

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

    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    return (Join-Path $layout.PythonCacheRoot 'pip')
}

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

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

    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    return (Test-ManifestedPathIsUnderRoot -Path $PythonExe -RootPath $layout.PythonToolsRoot)
}

function Get-ManifestedPythonPipConfiguration {
<#
.SYNOPSIS
Builds the effective pip configuration context for a Python command.
 
.DESCRIPTION
Determines whether the supplied Python executable belongs to a sandbox-managed
runtime and, when it does, resolves the runtime-local `pip.ini`,
`PIP_CONFIG_FILE`, and `PIP_CACHE_DIR` values that should be applied before
running pip-related commands.
 
.PARAMETER PythonExe
The Python executable path to inspect.
 
.PARAMETER LocalRoot
The pinned sandbox root used to derive managed cache and config locations.
 
.EXAMPLE
Get-ManifestedPythonPipConfiguration -PythonExe 'C:\Sandbox\tools\python\3.13.12\amd64\python.exe'
 
.EXAMPLE
Get-ManifestedPythonPipConfiguration -PythonExe 'C:\Python313\python.exe'
 
.NOTES
External Python runtimes return an empty environment-variable map.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PythonExe,

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

    $pythonHome = Split-Path -Parent $PythonExe
    $isManagedPython = Test-ManifestedManagedPythonCommand -PythonExe $PythonExe -LocalRoot $LocalRoot
    $pipConfigPath = $null
    $pipCacheRoot = $null
    $environmentVariables = [ordered]@{}

    if ($isManagedPython) {
        $pipConfigPath = Get-ManagedPythonPipConfigPath -PythonHome $pythonHome
        $pipCacheRoot = Get-ManagedPythonPipCacheRoot -LocalRoot $LocalRoot
        $environmentVariables['PIP_CONFIG_FILE'] = $pipConfigPath
        $environmentVariables['PIP_CACHE_DIR'] = $pipCacheRoot
    }

    [pscustomobject]@{
        IsManagedPython     = $isManagedPython
        PythonExe           = $PythonExe
        PythonHome          = $pythonHome
        PipConfigPath       = $pipConfigPath
        PipCacheRoot        = $pipCacheRoot
        EnvironmentVariables = $environmentVariables
    }
}

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

    return [uri]'https://pypi.org/simple/'
}

function Resolve-ManifestedPipProxyRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [uri]$IndexUri
    )

    $proxyUri = $null

    try {
        $systemProxy = [System.Net.WebRequest]::GetSystemWebProxy()
        if ($null -ne $systemProxy) {
            $systemProxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials

            if (-not $systemProxy.IsBypassed($IndexUri)) {
                $candidateProxyUri = $systemProxy.GetProxy($IndexUri)
                if ($null -ne $candidateProxyUri -and $candidateProxyUri.AbsoluteUri -ne $IndexUri.AbsoluteUri) {
                    $proxyUri = $candidateProxyUri
                }
            }
        }
    }
    catch {
        $proxyUri = $null
    }

    [pscustomobject]@{
        IndexUri      = $IndexUri.AbsoluteUri
        IndexHost     = $IndexUri.Host
        ProxyUri      = if ($proxyUri) { $proxyUri.AbsoluteUri } else { $null }
        ProxyRequired = ($null -ne $proxyUri)
        Route         = if ($proxyUri) { 'Proxy' } else { 'Direct' }
    }
}

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

    $document = [ordered]@{}
    if (-not (Test-Path -LiteralPath $ConfigPath)) {
        return $document
    }

    $currentSection = $null
    foreach ($rawLine in @(Get-Content -LiteralPath $ConfigPath -ErrorAction SilentlyContinue)) {
        $line = $rawLine.Trim()
        if ([string]::IsNullOrWhiteSpace($line)) {
            continue
        }
        if ($line.StartsWith(';') -or $line.StartsWith('#')) {
            continue
        }

        $sectionMatch = [regex]::Match($line, '^\[(.+)\]$')
        if ($sectionMatch.Success) {
            $currentSection = $sectionMatch.Groups[1].Value.Trim()
            if (-not $document.Contains($currentSection)) {
                $document[$currentSection] = [ordered]@{}
            }

            continue
        }

        $keyValueMatch = [regex]::Match($line, '^(?<key>[^=]+?)\s*=\s*(?<value>.*)$')
        if ($keyValueMatch.Success -and -not [string]::IsNullOrWhiteSpace($currentSection)) {
            $document[$currentSection][$keyValueMatch.Groups['key'].Value.Trim()] = $keyValueMatch.Groups['value'].Value.Trim()
        }
    }

    return $document
}

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

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

        [Parameter(Mandatory = $true)]
        [string]$Key
    )

    $document = Get-ManifestedPipConfigDocument -ConfigPath $ConfigPath
    if (-not $document.Contains($Section)) {
        return $null
    }
    if (-not $document[$Section].Contains($Key)) {
        return $null
    }

    return $document[$Section][$Key]
}

function Set-ManifestedPipConfigValue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ConfigPath,

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

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

        [Parameter(Mandatory = $true)]
        [string]$Value
    )

    $document = Get-ManifestedPipConfigDocument -ConfigPath $ConfigPath
    if (-not $document.Contains($Section)) {
        $document[$Section] = [ordered]@{}
    }

    $document[$Section][$Key] = $Value

    New-ManifestedDirectory -Path (Split-Path -Parent $ConfigPath) | Out-Null

    $lines = New-Object System.Collections.Generic.List[string]
    foreach ($sectionName in @($document.Keys)) {
        $lines.Add('[' + $sectionName + ']') | Out-Null
        foreach ($keyName in @($document[$sectionName].Keys)) {
            $lines.Add($keyName + ' = ' + $document[$sectionName][$keyName]) | Out-Null
        }
        $lines.Add('') | Out-Null
    }

    if ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') {
        $lines.RemoveAt($lines.Count - 1)
    }

    Set-Content -LiteralPath $ConfigPath -Value $lines -Encoding ASCII
    return $ConfigPath
}

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

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

    $pipConfiguration = Get-ManifestedPythonPipConfiguration -PythonExe $PythonExe -LocalRoot $LocalRoot
    $indexUri = Get-ManifestedPipIndexUri
    $proxyRoute = Resolve-ManifestedPipProxyRoute -IndexUri $indexUri
    $currentProxy = $null

    if (-not [string]::IsNullOrWhiteSpace($pipConfiguration.PipConfigPath)) {
        $currentProxy = Get-ManifestedPipConfigValue -ConfigPath $pipConfiguration.PipConfigPath -Section 'global' -Key 'proxy'
    }

    if (-not $pipConfiguration.IsManagedPython) {
        $action = 'SkippedExternalPip'
    }
    elseif ($proxyRoute.Route -eq 'Direct') {
        $action = 'DirectNoChange'
    }
    elseif ($currentProxy -eq $proxyRoute.ProxyUri) {
        $action = 'ReusedManagedProxy'
    }
    else {
        $action = 'NeedsManagedProxy'
    }

    [pscustomobject]@{
        PythonExe      = $pipConfiguration.PythonExe
        PythonHome     = $pipConfiguration.PythonHome
        IndexUri       = $proxyRoute.IndexUri
        IndexHost      = $proxyRoute.IndexHost
        ProxyUri       = $proxyRoute.ProxyUri
        ProxyRequired  = $proxyRoute.ProxyRequired
        Route          = $proxyRoute.Route
        Action         = $action
        PipConfigPath  = $pipConfiguration.PipConfigPath
        PipCacheRoot   = $pipConfiguration.PipCacheRoot
        CurrentProxy   = $currentProxy
    }
}

function Sync-ManifestedPipProxyConfiguration {
<#
.SYNOPSIS
Synchronizes the managed pip proxy setting with the effective system route.
 
.DESCRIPTION
Examines the current proxy route to the configured Python package index and, for
sandbox-managed Python runtimes, writes a runtime-local `pip.ini` only when a
proxy is actually required and the existing config does not already match.
 
.PARAMETER PythonExe
The Python executable that owns the managed pip configuration.
 
.PARAMETER Status
Optional precomputed proxy-status object from
`Get-ManifestedPipProxyConfigurationStatus`.
 
.PARAMETER LocalRoot
The pinned sandbox root used for managed cache/config resolution.
 
.EXAMPLE
Sync-ManifestedPipProxyConfiguration -PythonExe 'C:\Sandbox\tools\python\3.13.12\amd64\python.exe'
 
.EXAMPLE
$status = Get-ManifestedPipProxyConfigurationStatus -PythonExe $pythonExe
Sync-ManifestedPipProxyConfiguration -PythonExe $pythonExe -Status $status
 
.NOTES
External Python runtimes are left unchanged.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PythonExe,

        [pscustomobject]$Status,

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

    if (-not $Status) {
        $Status = Get-ManifestedPipProxyConfigurationStatus -PythonExe $PythonExe -LocalRoot $LocalRoot
    }

    if ($Status.Action -ne 'NeedsManagedProxy') {
        return $Status
    }

    if (-not [string]::IsNullOrWhiteSpace($Status.PipCacheRoot)) {
        New-ManifestedDirectory -Path $Status.PipCacheRoot | Out-Null
    }

    Set-ManifestedPipConfigValue -ConfigPath $Status.PipConfigPath -Section 'global' -Key 'proxy' -Value $Status.ProxyUri | Out-Null

    [pscustomobject]@{
        PythonExe      = $Status.PythonExe
        PythonHome     = $Status.PythonHome
        IndexUri       = $Status.IndexUri
        IndexHost      = $Status.IndexHost
        ProxyUri       = $Status.ProxyUri
        ProxyRequired  = $Status.ProxyRequired
        Route          = $Status.Route
        Action         = 'ConfiguredManagedProxy'
        PipConfigPath  = $Status.PipConfigPath
        PipCacheRoot   = $Status.PipCacheRoot
        CurrentProxy   = $Status.ProxyUri
    }
}

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

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

    $pipConfigPath = Get-ManagedPythonPipConfigPath -PythonHome $PythonHome
    $pipCacheRoot = Get-ManagedPythonPipCacheRoot -LocalRoot $LocalRoot
    New-ManifestedDirectory -Path $pipCacheRoot | Out-Null

    $wrapperLines = @(
        '@echo off',
        'setlocal',
        'set "PIP_CONFIG_FILE=%~dp0pip.ini"',
        'set "PIP_CACHE_DIR=' + $pipCacheRoot + '"',
        '"%~dp0python.exe" -m pip %*',
        'exit /b %ERRORLEVEL%'
    )

    $wrapperPaths = @(
        (Join-Path $PythonHome 'pip.cmd'),
        (Join-Path $PythonHome 'pip3.cmd')
    )

    foreach ($wrapperPath in $wrapperPaths) {
        Set-Content -LiteralPath $wrapperPath -Value $wrapperLines -Encoding ASCII
    }

    [pscustomobject]@{
        PythonHome    = $PythonHome
        PipConfigPath = $pipConfigPath
        PipCacheRoot  = $pipCacheRoot
        WrapperPaths  = @($wrapperPaths)
    }
}

function Invoke-ManifestedPipAwarePythonCommand {
<#
.SYNOPSIS
Invokes Python with the sandbox-managed pip environment when applicable.
 
.DESCRIPTION
Temporarily applies the runtime-local pip configuration and cache environment
variables for a managed Python runtime, executes the requested Python command,
captures its combined output, and restores the previous process environment
afterward.
 
.PARAMETER PythonExe
The Python executable to run.
 
.PARAMETER Arguments
The argument vector passed to the Python process.
 
.PARAMETER LocalRoot
The pinned sandbox root used to derive managed pip settings.
 
.EXAMPLE
Invoke-ManifestedPipAwarePythonCommand -PythonExe $pythonExe -Arguments @('-m', 'pip', '--version')
 
.EXAMPLE
Invoke-ManifestedPipAwarePythonCommand -PythonExe $pythonExe -Arguments @($getPipScriptPath)
 
.NOTES
This helper is the common execution path for managed pip bootstrap and pip
inspection calls.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PythonExe,

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

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

    $configuration = Get-ManifestedPythonPipConfiguration -PythonExe $PythonExe -LocalRoot $LocalRoot
    $previousValues = @{}
    $output = @()
    $exitCode = 0

    try {
        foreach ($variableName in @($configuration.EnvironmentVariables.Keys)) {
            $previousValues[$variableName] = [System.Environment]::GetEnvironmentVariable($variableName, 'Process')
            [System.Environment]::SetEnvironmentVariable($variableName, $configuration.EnvironmentVariables[$variableName], 'Process')
            Set-Item -Path ('Env:' + $variableName) -Value $configuration.EnvironmentVariables[$variableName]
        }

        $output = @(& $PythonExe @Arguments 2>&1)
        $exitCode = $LASTEXITCODE
    }
    finally {
        foreach ($variableName in @($configuration.EnvironmentVariables.Keys)) {
            $previousValue = $previousValues[$variableName]
            [System.Environment]::SetEnvironmentVariable($variableName, $previousValue, 'Process')
            if ($null -eq $previousValue) {
                Remove-Item -Path ('Env:' + $variableName) -ErrorAction SilentlyContinue
            }
            else {
                Set-Item -Path ('Env:' + $variableName) -Value $previousValue
            }
        }
    }

    [pscustomobject]@{
        Output        = @($output)
        ExitCode      = $exitCode
        Configuration = $configuration
    }
}