Private/Logic/Eigenverft.Manifested.Sandbox.Runtime.Python.Package.ps1
|
<#
Eigenverft.Manifested.Sandbox.Runtime.Python.Package #> function Get-PythonReleaseDescriptionForFlavor { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Flavor ) switch ($Flavor) { 'amd64' { return 'Windows embeddable package (64-bit)' } 'arm64' { return 'Windows embeddable package (ARM64)' } default { throw "Unsupported Python flavor '$Flavor'." } } } function Get-PythonPersistedPackageDetails { [CmdletBinding()] param( [string]$LocalRoot = (Get-ManifestedLocalRoot) ) $commandState = Get-ManifestedCommandState -CommandName 'Initialize-PythonRuntime' -LocalRoot $LocalRoot if ($commandState -and $commandState.PSObject.Properties['Details']) { return $commandState.Details } return $null } function Get-PythonReleaseCandidates { [CmdletBinding()] param() $response = Invoke-WebRequestEx -Uri 'https://www.python.org/downloads/windows/' -Headers @{ 'User-Agent' = 'Eigenverft.Manifested.Sandbox' } -UseBasicParsing $releaseMatches = [regex]::Matches($response.Content, '(?is)<a href="/downloads/release/python-(?<slug>313\d+)/">Python (?<version>3\.13\.\d+) - (?<releaseDate>[^<]+)</a>') $items = New-Object System.Collections.Generic.List[object] $seenVersions = @{} foreach ($match in $releaseMatches) { $versionText = $match.Groups['version'].Value.Trim() if ([string]::IsNullOrWhiteSpace($versionText) -or $seenVersions.ContainsKey($versionText)) { continue } $versionObject = ConvertTo-PythonVersion -VersionText $versionText if (-not $versionObject -or -not (Test-PythonManagedReleaseVersion -Version $versionObject)) { continue } $seenVersions[$versionText] = $true $items.Add([pscustomobject]@{ ReleaseId = $match.Groups['slug'].Value.Trim() Version = $versionText ReleaseDate = $match.Groups['releaseDate'].Value.Trim() ReleaseUrl = ('https://www.python.org/downloads/release/python-{0}/' -f $match.Groups['slug'].Value.Trim()) }) | Out-Null } return @($items | Sort-Object -Descending -Property @{ Expression = { ConvertTo-PythonVersion -VersionText $_.Version } }) } function Get-PythonReleaseAssetDetails { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ReleaseUrl, [Parameter(Mandatory = $true)] [string]$Flavor ) $descriptionPattern = switch ($Flavor) { 'amd64' { 'Windows\s+embeddable\s+package\s+\(64-bit\)' } 'arm64' { 'Windows\s+embeddable\s+package\s+\(ARM64\)' } default { throw "Unsupported Python flavor '$Flavor'." } } $response = Invoke-WebRequestEx -Uri $ReleaseUrl -Headers @{ 'User-Agent' = 'Eigenverft.Manifested.Sandbox' } -UseBasicParsing $pattern = '(?is)<tr>\s*<td><a href="(?<url>[^"]+)">' + $descriptionPattern + '</a>.*?<td><code class="checksum">(?<checksumHtml>.*?)</code></td>\s*</tr>' $match = [regex]::Match($response.Content, $pattern) if (-not $match.Success) { throw "Could not find the Python embeddable package row for flavor '$Flavor' in $ReleaseUrl." } $downloadUrl = $match.Groups['url'].Value.Trim() $checksumText = ($match.Groups['checksumHtml'].Value -replace '<[^>]+>', '') $checksum = ($checksumText -replace '[^0-9a-fA-F]', '').ToLowerInvariant() if ($checksum.Length -ne 64) { throw "Could not resolve a trusted SHA256 checksum for '$downloadUrl'." } [pscustomobject]@{ DownloadUrl = $downloadUrl FileName = [System.IO.Path]::GetFileName(([uri]$downloadUrl).AbsolutePath) Sha256 = $checksum ShaSource = 'ReleaseHtml' } } function Get-PythonRelease { [CmdletBinding()] param( [string]$Flavor ) if ([string]::IsNullOrWhiteSpace($Flavor)) { $Flavor = Get-PythonFlavor } foreach ($candidate in @(Get-PythonReleaseCandidates)) { try { $assetDetails = Get-PythonReleaseAssetDetails -ReleaseUrl $candidate.ReleaseUrl -Flavor $Flavor return [pscustomobject]@{ ReleaseId = $candidate.ReleaseId Version = $candidate.Version Flavor = $Flavor FileName = $assetDetails.FileName Path = $null Source = 'online' Action = 'SelectedOnline' DownloadUrl = $assetDetails.DownloadUrl Sha256 = $assetDetails.Sha256 ShaSource = $assetDetails.ShaSource ReleaseUrl = $candidate.ReleaseUrl ReleaseDate = $candidate.ReleaseDate } } catch { continue } } throw 'Unable to determine the latest stable Python 3.13 embeddable release.' } function Get-CachedPythonRuntimePackages { [CmdletBinding()] param( [string]$Flavor, [string]$LocalRoot = (Get-ManifestedLocalRoot) ) if ([string]::IsNullOrWhiteSpace($Flavor)) { $Flavor = Get-PythonFlavor } $layout = Get-ManifestedLayout -LocalRoot $LocalRoot if (-not (Test-Path -LiteralPath $layout.PythonCacheRoot)) { return @() } $persistedDetails = Get-PythonPersistedPackageDetails -LocalRoot $layout.LocalRoot $pattern = '^python-(\d+\.\d+\.\d+)-embed-' + [regex]::Escape($Flavor) + '\.zip$' $items = Get-ChildItem -LiteralPath $layout.PythonCacheRoot -File -Filter '*.zip' -ErrorAction SilentlyContinue | Where-Object { $_.Name -match $pattern } | ForEach-Object { $sha256 = $null $downloadUrl = $null $shaSource = $null $releaseUrl = $null $releaseId = $null if ($persistedDetails -and $persistedDetails.PSObject.Properties['AssetName'] -and $persistedDetails.AssetName -eq $_.Name) { $sha256 = if ($persistedDetails.PSObject.Properties['Sha256']) { $persistedDetails.Sha256 } else { $null } $downloadUrl = if ($persistedDetails.PSObject.Properties['DownloadUrl']) { $persistedDetails.DownloadUrl } else { $null } $shaSource = if ($persistedDetails.PSObject.Properties['ShaSource']) { $persistedDetails.ShaSource } else { $null } $releaseUrl = if ($persistedDetails.PSObject.Properties['ReleaseUrl']) { $persistedDetails.ReleaseUrl } else { $null } $releaseId = if ($persistedDetails.PSObject.Properties['ReleaseId']) { $persistedDetails.ReleaseId } else { $null } } [pscustomobject]@{ ReleaseId = $releaseId Version = $matches[1] Flavor = $Flavor FileName = $_.Name Path = $_.FullName Source = 'cache' Action = 'SelectedCache' DownloadUrl = $downloadUrl Sha256 = $sha256 ShaSource = $shaSource ReleaseUrl = $releaseUrl ReleaseDate = $null } } | Sort-Object -Descending -Property @{ Expression = { ConvertTo-PythonVersion -VersionText $_.Version } } return @($items) } function Get-LatestCachedPythonRuntimePackage { [CmdletBinding()] param( [string]$Flavor, [string]$LocalRoot = (Get-ManifestedLocalRoot) ) $cachedPackages = @(Get-CachedPythonRuntimePackages -Flavor $Flavor -LocalRoot $LocalRoot) $trustedPackage = @($cachedPackages | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Sha256) } | Select-Object -First 1) if ($trustedPackage) { return $trustedPackage[0] } return ($cachedPackages | Select-Object -First 1) } function Test-PythonRuntimePackageHasTrustedHash { [CmdletBinding()] param( [pscustomobject]$PackageInfo ) return ($PackageInfo -and -not [string]::IsNullOrWhiteSpace($PackageInfo.Sha256)) } function Test-PythonRuntimePackage { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$PackageInfo ) if (-not (Test-Path -LiteralPath $PackageInfo.Path)) { return [pscustomobject]@{ Status = 'Missing' ReleaseId = if ($PackageInfo.PSObject.Properties['ReleaseId']) { $PackageInfo.ReleaseId } else { $null } Version = $PackageInfo.Version Flavor = $PackageInfo.Flavor FileName = $PackageInfo.FileName Path = $PackageInfo.Path Source = $PackageInfo.Source Verified = $false Verification = 'Missing' ExpectedHash = $null ActualHash = $null } } if ([string]::IsNullOrWhiteSpace($PackageInfo.Sha256)) { return [pscustomobject]@{ Status = 'UnverifiedCache' ReleaseId = if ($PackageInfo.PSObject.Properties['ReleaseId']) { $PackageInfo.ReleaseId } else { $null } Version = $PackageInfo.Version Flavor = $PackageInfo.Flavor FileName = $PackageInfo.FileName Path = $PackageInfo.Path Source = $PackageInfo.Source Verified = $false Verification = 'MissingTrustedHash' ExpectedHash = $null ActualHash = $null } } $actualHash = (Get-FileHash -LiteralPath $PackageInfo.Path -Algorithm SHA256).Hash.ToLowerInvariant() $expectedHash = $PackageInfo.Sha256.ToLowerInvariant() [pscustomobject]@{ Status = if ($actualHash -eq $expectedHash) { 'Ready' } else { 'CorruptCache' } ReleaseId = if ($PackageInfo.PSObject.Properties['ReleaseId']) { $PackageInfo.ReleaseId } else { $null } Version = $PackageInfo.Version Flavor = $PackageInfo.Flavor FileName = $PackageInfo.FileName Path = $PackageInfo.Path Source = $PackageInfo.Source Verified = $true Verification = if ($PackageInfo.PSObject.Properties['ShaSource'] -and $PackageInfo.ShaSource) { $PackageInfo.ShaSource } else { 'SHA256' } ExpectedHash = $expectedHash ActualHash = $actualHash } } function Save-PythonRuntimePackage { [CmdletBinding()] param( [switch]$RefreshPython, [string]$Flavor, [string]$LocalRoot = (Get-ManifestedLocalRoot) ) if ([string]::IsNullOrWhiteSpace($Flavor)) { $Flavor = Get-PythonFlavor } $layout = Get-ManifestedLayout -LocalRoot $LocalRoot New-ManifestedDirectory -Path $layout.PythonCacheRoot | Out-Null $release = $null try { $release = Get-PythonRelease -Flavor $Flavor } catch { $release = $null } if ($release) { $packagePath = Join-Path $layout.PythonCacheRoot $release.FileName $downloadPath = Get-ManifestedDownloadPath -TargetPath $packagePath $action = 'ReusedCache' if ($RefreshPython -or -not (Test-Path -LiteralPath $packagePath)) { Remove-ManifestedPath -Path $downloadPath | Out-Null try { Write-Host "Downloading Python $($release.Version) embeddable runtime ($Flavor)..." Enable-ManifestedTls12Support Invoke-WebRequestEx -Uri $release.DownloadUrl -Headers @{ 'User-Agent' = 'Eigenverft.Manifested.Sandbox' } -OutFile $downloadPath -UseBasicParsing Move-Item -LiteralPath $downloadPath -Destination $packagePath -Force $action = 'Downloaded' } catch { Remove-ManifestedPath -Path $downloadPath | Out-Null if (-not (Test-Path -LiteralPath $packagePath)) { throw } Write-Warning ('Could not refresh the Python runtime package. Using cached copy. ' + $_.Exception.Message) $action = 'ReusedCache' } } return [pscustomobject]@{ ReleaseId = $release.ReleaseId Version = $release.Version Flavor = $Flavor FileName = $release.FileName Path = $packagePath Source = if ($action -eq 'Downloaded') { 'online' } else { 'cache' } Action = $action DownloadUrl = $release.DownloadUrl Sha256 = $release.Sha256 ShaSource = $release.ShaSource ReleaseUrl = $release.ReleaseUrl ReleaseDate = $release.ReleaseDate } } $cachedPackage = Get-LatestCachedPythonRuntimePackage -Flavor $Flavor -LocalRoot $LocalRoot if (-not $cachedPackage) { throw 'Could not reach python.org and no cached Python embeddable ZIP was found.' } return [pscustomobject]@{ ReleaseId = $cachedPackage.ReleaseId Version = $cachedPackage.Version Flavor = $cachedPackage.Flavor FileName = $cachedPackage.FileName Path = $cachedPackage.Path Source = 'cache' Action = 'SelectedCache' DownloadUrl = $cachedPackage.DownloadUrl Sha256 = $cachedPackage.Sha256 ShaSource = $cachedPackage.ShaSource ReleaseUrl = $cachedPackage.ReleaseUrl ReleaseDate = $cachedPackage.ReleaseDate } } function Resolve-PythonRuntimeTrustedPackageInfo { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$PackageInfo, [string]$Flavor, [string]$LocalRoot = (Get-ManifestedLocalRoot) ) if (Test-PythonRuntimePackageHasTrustedHash -PackageInfo $PackageInfo) { return [pscustomobject]@{ PackageInfo = $PackageInfo MetadataRefreshError = $null MetadataRefreshTried = $false UsedTrustedPackage = $true } } if ([string]::IsNullOrWhiteSpace($Flavor)) { $Flavor = if ($PackageInfo.PSObject.Properties['Flavor'] -and $PackageInfo.Flavor) { $PackageInfo.Flavor } else { Get-PythonFlavor } } $refreshedPackage = $null $metadataRefreshError = $null try { $refreshedPackage = Save-PythonRuntimePackage -RefreshPython:$false -Flavor $Flavor -LocalRoot $LocalRoot } catch { $metadataRefreshError = $_.Exception.Message } if ($refreshedPackage -and (Test-Path -LiteralPath $PackageInfo.Path) -and (Test-PythonRuntimePackageHasTrustedHash -PackageInfo $refreshedPackage)) { $samePackagePath = ((Get-ManifestedFullPath -Path $refreshedPackage.Path) -eq (Get-ManifestedFullPath -Path $PackageInfo.Path)) $samePackageName = ($refreshedPackage.FileName -eq $PackageInfo.FileName) if ($samePackagePath -or $samePackageName) { return [pscustomobject]@{ PackageInfo = $refreshedPackage MetadataRefreshError = $metadataRefreshError MetadataRefreshTried = $true UsedTrustedPackage = $true } } } return [pscustomobject]@{ PackageInfo = $PackageInfo MetadataRefreshError = $metadataRefreshError MetadataRefreshTried = $true UsedTrustedPackage = (Test-PythonRuntimePackageHasTrustedHash -PackageInfo $PackageInfo) } } function New-PythonRuntimePackageTrustFailureMessage { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$PackageInfo, [string]$MetadataRefreshError ) $lines = New-Object System.Collections.Generic.List[string] $lines.Add(("Python runtime package verification could not establish a trusted SHA256 for cached package '{0}'." -f $PackageInfo.FileName)) | Out-Null $lines.Add(("Cached path: {0}" -f $PackageInfo.Path)) | Out-Null if ($PackageInfo.PSObject.Properties['Version'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.Version)) { $lines.Add(("Cached version: {0}" -f $PackageInfo.Version)) | Out-Null } if ($PackageInfo.PSObject.Properties['Flavor'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.Flavor)) { $lines.Add(("Cached flavor: {0}" -f $PackageInfo.Flavor)) | Out-Null } if ($PackageInfo.PSObject.Properties['Source'] -and -not [string]::IsNullOrWhiteSpace($PackageInfo.Source)) { $lines.Add(("Package source: {0}" -f $PackageInfo.Source)) | Out-Null } $lines.Add('The cached ZIP is present, but this run does not have trusted release metadata attached to verify it.') | Out-Null $lines.Add('This usually happens when an earlier Python bootstrap downloaded the ZIP but failed before package metadata was persisted.') | Out-Null if (-not [string]::IsNullOrWhiteSpace($MetadataRefreshError)) { $lines.Add(("Metadata refresh attempt failed: {0}" -f $MetadataRefreshError)) | Out-Null } else { $lines.Add('A metadata refresh attempt did not produce a trusted checksum for the cached ZIP.') | Out-Null } $lines.Add('Retry with normal network access so python.org release metadata can be resolved, or use Initialize-PythonRuntime -RefreshPython to reacquire the package.') | Out-Null return (@($lines) -join [Environment]::NewLine) } |