#Requires -Version 5.1 <# Shared build helpers for Proxy God / ProxyChainManager. Dot-sourced by setup_and_build.ps1 and release_build.ps1 — do not run directly. #> Set-StrictMode -Version Latest function Get-RepoRoot { # This file lives in scripts/ — parent is the repository root. return (Split-Path -Parent $PSScriptRoot) } function Resolve-PythonExe { if (Get-Command py -ErrorAction SilentlyContinue) { try { $out = (& py -3 -c "import sys; print(sys.executable)" 2>$null).Trim() if ($out -and (Test-Path -LiteralPath $out)) { return $out } } catch { } } $p = Get-Command python.exe -ErrorAction SilentlyContinue if ($p) { return $p.Source } throw @( "Python 3.10+ not found (tried 'py -3' and 'python'). Install, then re-run:", " winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements", "Or https://www.python.org/downloads/ (enable 'Add python.exe to PATH')." ) -join "`n" } function Get-GitCommit { try { $c = (git -C (Get-RepoRoot) rev-parse --short HEAD 2>$null).Trim() if ($c) { return $c } } catch { } return "unknown" } function Get-ReleaseVersion { param([string]$Override = "") if ($Override) { return $Override.Trim() } $root = Get-RepoRoot try { $tag = (git -C $root describe --tags --exact-match 2>$null).Trim() if ($tag) { return $tag.TrimStart("v") } } catch { } try { $desc = (git -C $root describe --tags --always --dirty 2>$null).Trim() if ($desc) { return ($desc -replace '^v', '') } } catch { } $py = Resolve-PythonExe $v = (& $py -c "from proxy_chain_manager import __version__; print(__version__)" 2>$null).Trim() if ($v) { return $v } return "0.0.0-dev" } function Install-BuildDependencies { param([string]$PythonExe, [string]$Root) Write-Host "`n== pip (upgrade) ==" & $PythonExe -m pip install --upgrade pip Write-Host "`n== runtime dependencies ==" & $PythonExe -m pip install -r (Join-Path $Root "requirements.txt") Write-Host "`n== build dependencies ==" & $PythonExe -m pip install -r (Join-Path $Root "dev-requirements.txt") } function Invoke-UnitTests { param([string]$PythonExe, [string]$Root) Write-Host "`n== compileall ==" & $PythonExe -m compileall -q (Join-Path $Root "proxy_chain_manager") if ($LASTEXITCODE -ne 0) { throw "compileall failed (exit $LASTEXITCODE)" } Write-Host "`n== unittest discover ==" Push-Location $Root try { & $PythonExe -m unittest discover -s tests -v if ($LASTEXITCODE -ne 0) { throw "unittest failed (exit $LASTEXITCODE)" } } finally { Pop-Location } } function Invoke-StageBundledGost { param([string]$Root) Write-Host "`n== Stage bundled GOST ==" $prep = Join-Path $Root "scripts\prepare_bundled_gost.ps1" & powershell -NoProfile -ExecutionPolicy Bypass -File $prep if ($LASTEXITCODE -ne 0) { throw "prepare_bundled_gost.ps1 failed (exit $LASTEXITCODE)" } } function Invoke-GenerateVersionInfo { param( [string]$PythonExe, [string]$Root, [string]$Version, [string]$Commit = "" ) Write-Host "`n== Version metadata ($Version) ==" $gen = Join-Path $Root "scripts\generate_version_info.py" $out = Join-Path $Root "build\version_info.txt" $args = @($gen, "--version", $Version, "--out", $out) if ($Commit) { $args += @("--commit", $Commit) } & $PythonExe @args if ($LASTEXITCODE -ne 0) { throw "generate_version_info.py failed (exit $LASTEXITCODE)" } } function Invoke-PyInstallerBuild { param([string]$PythonExe, [string]$Root) Write-Host "`n== PyInstaller (ProxyChainManager.spec) ==" $spec = Join-Path $Root "ProxyChainManager.spec" if (-not (Test-Path $spec)) { throw "Spec not found: $spec" } & $PythonExe -m PyInstaller --noconfirm --clean $spec if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed (exit $LASTEXITCODE)" } $exe = Join-Path $Root "dist\ProxyChainManager.exe" if (-not (Test-Path $exe)) { throw "Build failed: missing $exe" } return $exe } function Invoke-CodeSign { param([string]$ExePath) $cert = $env:SIGN_CERT_PATH if (-not $cert) { Write-Host " (signing skipped — set SIGN_CERT_PATH to a .pfx to Authenticode-sign)" return } if (-not (Test-Path $cert)) { throw "SIGN_CERT_PATH not found: $cert" } $signtool = "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" if (-not (Test-Path $signtool)) { $signtool = (Get-Command signtool.exe -ErrorAction SilentlyContinue).Source } if (-not $signtool) { throw "signtool.exe not found — install Windows SDK" } Write-Host "`n== Authenticode sign ==" $args = @("sign", "/fd", "SHA256", "/f", $cert, "/tr", "http://timestamp.digicert.com", "/td", "SHA256") if ($env:SIGN_CERT_PASSWORD) { $args += @("/p", $env:SIGN_CERT_PASSWORD) } $args += $ExePath & $signtool @args if ($LASTEXITCODE -ne 0) { throw "signtool sign failed (exit $LASTEXITCODE)" } } function Write-ExeHashSidecar { param([string]$ExePath) $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $ExePath).Hash.ToLower() $sidecar = "$ExePath.sha256" "$hash *$(Split-Path -Leaf $ExePath)" | Out-File -FilePath $sidecar -Encoding ascii -Force Write-Host "SHA256: $hash -> $sidecar" return @{ Hash = $hash; Sidecar = $sidecar } } function Write-Sbom { param( [string]$PythonExe, [string]$OutDir, [string]$Version ) Write-Host "`n== SBOM ==" $freeze = Join-Path $OutDir "requirements-frozen.txt" & $PythonExe -m pip freeze | Out-File -FilePath $freeze -Encoding utf8 $sbom = Join-Path $OutDir "SBOM.json" $pkgs = @() Get-Content $freeze | ForEach-Object { if ($_ -match '^([^=]+)==(.+)$') { $pkgs += @{ name = $Matches[1]; version = $Matches[2] } } } $manifest = @{ product = "Proxy God" version = $Version generated = (Get-Date).ToUniversalTime().ToString("o") python = (& $PythonExe -c "import sys; print(sys.version.split()[0])").Trim() packages = $pkgs } ($manifest | ConvertTo-Json -Depth 4) | Out-File -FilePath $sbom -Encoding utf8 Write-Host "Wrote $sbom and $freeze" return @{ Sbom = $sbom; Freeze = $freeze } } function Write-ReleaseManifest { param( [string]$OutDir, [string]$Version, [string]$Commit, [string]$ExePath, [string]$Sha256, [hashtable]$Extra = @{} ) $manifest = @{ product = "Proxy God" executable = "ProxyChainManager.exe" version = $Version git_commit = $Commit built_at_utc = (Get-Date).ToUniversalTime().ToString("o") platform = "windows-amd64" sha256 = $Sha256 uac_admin = $true bundled_gost = $true pyinstaller = "6.10.0" } foreach ($k in $Extra.Keys) { $manifest[$k] = $Extra[$k] } $path = Join-Path $OutDir "RELEASE_MANIFEST.json" ($manifest | ConvertTo-Json -Depth 4) | Out-File -FilePath $path -Encoding utf8 Write-Host "Wrote $path" return $path } function New-ReleaseZip { param( [string]$ReleaseDir, [string]$Version, [string]$Root ) $zipName = "ProxyGod-v$Version-windows-amd64.zip" $zipPath = Join-Path $Root "releases\$zipName" $releasesRoot = Split-Path $zipPath -Parent if (-not (Test-Path $releasesRoot)) { New-Item -ItemType Directory -Path $releasesRoot | Out-Null } if (Test-Path $zipPath) { Remove-Item -LiteralPath $zipPath -Force } Compress-Archive -Path (Join-Path $ReleaseDir "*") -DestinationPath $zipPath -Force Write-Host "Release zip: $zipPath" return $zipPath }