From 08683ab328ce2db63c8f917fe3d2a0b2ad38140a Mon Sep 17 00:00:00 2001 From: Dr Jones Date: Fri, 22 May 2026 20:05:00 -0700 Subject: [PATCH] build: production Windows release pipeline (PyInstaller, SBOM, CI, signing hook) --- .github/workflows/release.yml | 56 ++++++++ .gitignore | 4 + ProxyChainManager.spec | 20 ++- README.md | 63 ++++++--- build_release.bat | 27 ++++ docs/RELEASE_PROCESS.md | 100 ++++++++++++-- scripts/_build_common.ps1 | 221 +++++++++++++++++++++++++++++++ scripts/generate_version_info.py | 74 +++++++++++ scripts/release_build.ps1 | 108 +++++++++++++++ scripts/setup_and_build.ps1 | 81 ++++------- 10 files changed, 659 insertions(+), 95 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 build_release.bat create mode 100644 scripts/_build_common.ps1 create mode 100644 scripts/generate_version_info.py create mode 100644 scripts/release_build.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..253b531 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release Build + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + version: + description: "Optional version override (e.g. 1.2.0)" + required: false + default: "" + +jobs: + release: + name: Build Windows release + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Production release build + shell: pwsh + run: | + $args = @() + if ("${{ github.event.inputs.version }}") { $args += "-Version", "${{ github.event.inputs.version }}" } + & ./scripts/release_build.ps1 @args + + - name: Upload release artifacts + uses: actions/upload-artifact@v4 + with: + name: ProxyGod-windows-amd64 + path: | + releases/**/* + dist/ProxyChainManager.exe + dist/ProxyChainManager.exe.sha256 + if-no-files-found: error + + - name: Create GitHub Release (tag push only) + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: | + releases/**/*.zip + releases/**/ProxyChainManager.exe + releases/**/ProxyChainManager.exe.sha256 + releases/**/RELEASE_MANIFEST.json + releases/**/SBOM.json + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 5b42fd8..a4e53d7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ signup_draft.json # (downloaded from go-gost release with verified SHA256), not source code proxy_chain_manager/_bundled/ +# Release output (scripts/release_build.ps1) — upload as CI artifacts / GitHub Release +releases/ +build/version_info.txt + diff --git a/ProxyChainManager.spec b/ProxyChainManager.spec index d8d7416..3559a57 100644 --- a/ProxyChainManager.spec +++ b/ProxyChainManager.spec @@ -56,12 +56,9 @@ a = Analysis( ) pyz = PYZ(a.pure) -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.datas, - [], +# Windows VERSIONINFO — generated by scripts/generate_version_info.py before build. +_version_file = _ROOT / 'build' / 'version_info.txt' +_exe_kwargs = dict( name='ProxyChainManager', debug=False, bootloader_ignore_signals=False, @@ -77,3 +74,14 @@ exe = EXE( entitlements_file=None, uac_admin=True, ) +if _version_file.is_file(): + _exe_kwargs['version'] = str(_version_file) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + **_exe_kwargs, +) diff --git a/README.md b/README.md index 97802e2..64a4297 100644 --- a/README.md +++ b/README.md @@ -66,35 +66,56 @@ YOU -> VPN (outer tunnel) -> Hop 1 -> Hop 2 -> ... -> Exit hop -> Internet --- -## Build to Desktop (recommended) +## Build (Windows) -1. Clone (or `git pull` in existing repo): - ```cmd - git clone https://gitea.thetempleofdoom.com/drjones/proxy-god.git - cd proxy-god - ``` -2. Install Python if needed: - ```cmd - winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements - ``` -3. Build: - ```powershell - powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\setup_and_build.ps1 - ``` +Requires **Python 3.10+** and **Windows 10/11 x64**. Python is not needed to *run* the built `.exe`. -Equivalent paths: -- `build_exe.bat` -- `FirstRun_Build_And_Install.bat` +```cmd +winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements +``` -Artifacts: +### Production release (recommended for shipping) + +Full pipeline: tests → bundled GOST → PyInstaller → SHA256 + SBOM + manifest → zip. + +```cmd +build_release.bat +``` + +Or: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\release_build.ps1 +``` + +Output: `releases\ProxyGod-v{version}-windows-amd64.zip` and `releases\v{version}\` (exe, checksum, SBOM, manifest). See [docs/RELEASE_PROCESS.md](docs/RELEASE_PROCESS.md). + +Tag-driven CI build (GitHub): + +```bash +git tag v1.0.0 +git push origin v1.0.0 +``` + +### Developer build (Desktop copy) + +Faster iteration — copies `dist\ProxyChainManager.exe` to Desktop + shortcut. + +```cmd +build_exe.bat +``` + +Or `FirstRun_Build_And_Install.bat` (same script, pauses on error). | Path | Purpose | |------|------| | `dist\ProxyChainManager.exe` | Built binary | -| `Desktop\ProxyChainManager.exe` | Refreshed desktop copy | -| `Desktop\Proxy God.lnk` | Shortcut target for daily use | +| `releases\` | Production release folders + zip (from `release_build.ps1`) | +| `Desktop\ProxyChainManager.exe` | Dev build desktop copy | +| `Desktop\Proxy God.lnk` | Shortcut | + +Recreate shortcut only: -Recreate missing shortcut: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\create_desktop_shortcut.ps1 ``` diff --git a/build_release.bat b/build_release.bat new file mode 100644 index 0000000..68b5c62 --- /dev/null +++ b/build_release.bat @@ -0,0 +1,27 @@ +@echo off +setlocal +title Proxy God — production release build +cd /d "%~dp0" + +echo. +echo === Proxy God: production release (tests + SBOM + zip) === +echo. + +where py >nul 2>nul && py -3 -c "import sys; assert sys.version_info>=(3,10)" 2>nul && goto RUN +where python >nul 2>nul && python -c "import sys; assert sys.version_info>=(3,10)" 2>nul && goto RUN + +echo [X] Python 3.10+ not found on PATH. +pause +exit /b 1 + +:RUN +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\release_build.ps1" %* +if errorlevel 1 ( + echo. + echo Release build FAILED. + pause + exit /b 1 +) +echo. +pause +endlocal diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 3a5ca27..0a90369 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -1,20 +1,92 @@ # Release Process - -## Before Tagging +Production Windows builds use **PyInstaller** with a gated pipeline in `scripts/release_build.ps1`. -- Confirm the default branch builds, runs, or flashes as documented. -- Confirm no secrets, private data, generated dependency trees, or raw binaries are accidentally committed. -- Confirm license and upstream provenance are documented. -- Update CHANGELOG.md. -- Attach binaries only as release assets with SHA256 checksums and source commit references. +## Quick commands -## Release Notes +| Goal | Command | +|------|---------| +| **Production release** | `build_release.bat` or `powershell -File .\scripts\release_build.ps1` | +| **Dev build (Desktop copy)** | `build_exe.bat` or `powershell -File .\scripts\setup_and_build.ps1` | +| **Skip tests (dev only)** | `powershell -File .\scripts\setup_and_build.ps1 -SkipTests` | -Include: +## Production pipeline (`release_build.ps1`) -- Purpose of the release. -- Commit hash or tag. -- Build environment. -- Known limitations. -- Verification performed. +1. Resolve Python 3.10+ +2. Install `requirements.txt` + `dev-requirements.txt` (PyInstaller **6.10.0** pinned) +3. **Test gate**: `compileall` + `unittest discover` (skip with `-SkipTests` — not for prod) +4. Download & SHA-verify bundled `gost.exe` → `proxy_chain_manager/_bundled/` +5. Generate Windows **VERSIONINFO** → `build/version_info.txt` +6. **PyInstaller** one-file build via `ProxyChainManager.spec` +7. Optional **Authenticode** sign (see below) +8. SHA256 sidecar for the exe +9. **SBOM** (`SBOM.json` + `requirements-frozen.txt`) +10. **RELEASE_MANIFEST.json** (version, commit, sha256, build time) +11. Zip → `releases/ProxyGod-v{version}-windows-amd64.zip` + +### Output layout + +``` +releases/ + v1.0.0/ + ProxyChainManager.exe + ProxyChainManager.exe.sha256 + RELEASE_MANIFEST.json + SBOM.json + requirements-frozen.txt + LICENSE + OPERATOR_RUNBOOK.md + ProxyGod-v1.0.0-windows-amd64.zip +dist/ + ProxyChainManager.exe # same binary (developer convenience) + ProxyChainManager.exe.sha256 +``` + +## Version numbering + +Resolved in order: + +1. `-Version` parameter to `release_build.ps1` +2. Exact git tag on current commit (`git describe --tags --exact-match`) +3. `git describe --tags --always --dirty` +4. `proxy_chain_manager.__version__` + +Tag releases with `v1.2.3` — CI **release.yml** runs automatically on `v*` tags. + +## Authenticode signing (optional) + +Set before building: + +```powershell +$env:SIGN_CERT_PATH = "C:\certs\proxygod.pfx" +$env:SIGN_CERT_PASSWORD = "your-password" # optional if pfx has no password +powershell -File .\scripts\release_build.ps1 +``` + +Requires **Windows SDK** (`signtool.exe` on PATH). Without a cert, the build completes unsigned (SmartScreen may warn on first run). + +## CI / GitHub Releases + +- **Every push/PR**: `.github/workflows/test.yml` — unit tests only +- **Tag `v*` or manual dispatch**: `.github/workflows/release.yml` — full release build + artifact upload + GitHub Release assets + +```bash +git tag v1.0.0 +git push origin v1.0.0 +``` + +## Before tagging (checklist) + +- [ ] `python -m unittest discover -s tests -v` passes locally +- [ ] CHANGELOG.md updated +- [ ] No secrets in `settings.json` / signup JSON committed +- [ ] `proxy_chain_manager/_bundled/gost.exe` will be fetched at build time (or pre-staged) +- [ ] Verify SHA256 after build: `Get-FileHash releases\v*\ProxyChainManager.exe -Algorithm SHA256` + +## Verify a release artifact + +```powershell +Get-FileHash -Algorithm SHA256 releases\v1.0.0\ProxyChainManager.exe +Get-Content releases\v1.0.0\ProxyChainManager.exe.sha256 +Get-Content releases\v1.0.0\RELEASE_MANIFEST.json | ConvertFrom-Json +``` diff --git a/scripts/_build_common.ps1 b/scripts/_build_common.ps1 new file mode 100644 index 0000000..2785fdf --- /dev/null +++ b/scripts/_build_common.ps1 @@ -0,0 +1,221 @@ +#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 +} diff --git a/scripts/generate_version_info.py b/scripts/generate_version_info.py new file mode 100644 index 0000000..5cddcdb --- /dev/null +++ b/scripts/generate_version_info.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Generate a PyInstaller version-info file for ProxyChainManager.exe. + +Usage: + python scripts/generate_version_info.py --version 1.2.3 --out build/version_info.txt + +The output is consumed by ProxyChainManager.spec when the file exists. +""" +from __future__ import annotations + +import argparse +import re +from pathlib import Path + + +def _parse_version(version: str) -> tuple[int, int, int, int]: + parts = re.findall(r"\d+", version) + nums = [int(x) for x in parts[:4]] + while len(nums) < 4: + nums.append(0) + return nums[0], nums[1], nums[2], nums[3] + + +def build_version_info(version: str, commit: str = "") -> str: + filevers = prodvers = _parse_version(version) + commit_suffix = f" ({commit})" if commit else "" + return f"""# UTF-8 +# Generated by scripts/generate_version_info.py — do not edit by hand. +VSVersionInfo( + ffi=FixedFileInfo( + filevers={filevers}, + prodvers={prodvers}, + mask=0x3f, + flags=0x0, + OS=0x40004, + fileType=0x1, + subtype=0x0, + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + u'040904B0', + [StringStruct(u'CompanyName', u'Proxy God'), + StringStruct(u'FileDescription', u'Proxy God — multi-hop proxy chain manager'), + StringStruct(u'FileVersion', u'{version}{commit_suffix}'), + StringStruct(u'InternalName', u'ProxyChainManager'), + StringStruct(u'LegalCopyright', u'MIT License'), + StringStruct(u'OriginalFilename', u'ProxyChainManager.exe'), + StringStruct(u'ProductName', u'Proxy God'), + StringStruct(u'ProductVersion', u'{version}{commit_suffix}')]) + ] + ), + VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) + ] +) +""" + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--version", required=True) + ap.add_argument("--commit", default="") + ap.add_argument("--out", required=True) + args = ap.parse_args() + out = Path(args.out) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(build_version_info(args.version, args.commit), encoding="utf-8") + print(f"Wrote {out}") + + +if __name__ == "__main__": + main() diff --git a/scripts/release_build.ps1 b/scripts/release_build.ps1 new file mode 100644 index 0000000..40b3b8a --- /dev/null +++ b/scripts/release_build.ps1 @@ -0,0 +1,108 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Production release build for Proxy God (ProxyChainManager.exe). + +.DESCRIPTION + Full pipeline: + 1. Resolve Python 3.10+ + 2. Install pinned runtime + build deps + 3. Run compileall + unittest (gate) + 4. Stage SHA-verified bundled gost.exe + 5. Generate Windows VERSIONINFO for the exe + 6. PyInstaller one-file build (ProxyChainManager.spec) + 7. Optional Authenticode sign (SIGN_CERT_PATH / SIGN_CERT_PASSWORD) + 8. SHA256 sidecar + SBOM + RELEASE_MANIFEST.json + 9. Zip release folder -> releases/ProxyGod-v{version}-windows-amd64.zip + +.PARAMETER Version + Override semver (default: git tag > git describe > __version__). + +.PARAMETER SkipTests + Skip compileall/unittest (not recommended for production). + +.PARAMETER SkipSign + Skip Authenticode even if SIGN_CERT_PATH is set. + +.PARAMETER NoZip + Skip creating the release zip archive. + +.EXAMPLE + powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\release_build.ps1 + +.EXAMPLE + powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\release_build.ps1 -Version 1.2.0 +#> +[CmdletBinding()] +param( + [string]$Version = "", + [switch]$SkipTests, + [switch]$SkipSign, + [switch]$NoZip +) + +$ErrorActionPreference = "Stop" +. "$PSScriptRoot\_build_common.ps1" + +$root = Get-RepoRoot +Set-Location $root + +$py = Resolve-PythonExe +Write-Host "Using: $py" +& $py -c "import sys; assert sys.version_info >= (3, 10); print(sys.version)" + +$ver = Get-ReleaseVersion -Override $Version +$commit = Get-GitCommit +Write-Host "Release version: $ver (commit $commit)" + +Install-BuildDependencies -PythonExe $py -Root $root + +if (-not $SkipTests) { + Invoke-UnitTests -PythonExe $py -Root $root +} else { + Write-Warning "Skipping test gate (-SkipTests)" +} + +Invoke-StageBundledGost -Root $root +Invoke-GenerateVersionInfo -PythonExe $py -Root $root -Version $ver -Commit $commit + +$exe = Invoke-PyInstallerBuild -PythonExe $py -Root $root + +if (-not $SkipSign) { + Invoke-CodeSign -ExePath $exe +} + +$hashInfo = Write-ExeHashSidecar -ExePath $exe + +# Assemble release directory (versioned folder under releases/) +$releaseDir = Join-Path $root "releases\v$ver" +if (Test-Path $releaseDir) { Remove-Item -Recurse -Force $releaseDir } +New-Item -ItemType Directory -Path $releaseDir | Out-Null + +Copy-Item -LiteralPath $exe -Destination (Join-Path $releaseDir "ProxyChainManager.exe") -Force +Copy-Item -LiteralPath $hashInfo.Sidecar -Destination (Join-Path $releaseDir "ProxyChainManager.exe.sha256") -Force +Copy-Item -LiteralPath (Join-Path $root "LICENSE") -Destination $releaseDir -ErrorAction SilentlyContinue +Copy-Item -LiteralPath (Join-Path $root "docs\OPERATOR_RUNBOOK.md") -Destination $releaseDir -ErrorAction SilentlyContinue + +$sbom = Write-Sbom -PythonExe $py -OutDir $releaseDir -Version $ver +Write-ReleaseManifest -OutDir $releaseDir -Version $ver -Commit $commit ` + -ExePath $exe -Sha256 $hashInfo.Hash -Extra @{ + sbom_file = (Split-Path -Leaf $sbom.Sbom) + requirements_frozen = (Split-Path -Leaf $sbom.Freeze) + } + +# Also refresh dist/ sidecars for developers +Copy-Item -LiteralPath $hashInfo.Sidecar -Destination (Join-Path $root "dist\ProxyChainManager.exe.sha256") -Force + +if (-not $NoZip) { + $zip = New-ReleaseZip -ReleaseDir $releaseDir -Version $ver -Root $root + Write-Host "`n== Release complete ==" + Write-Host " Folder: $releaseDir" + Write-Host " Zip: $zip" +} else { + Write-Host "`n== Release complete (no zip) ==" + Write-Host " Folder: $releaseDir" +} + +Write-Host "`nVerify:" +Write-Host " Get-FileHash -Algorithm SHA256 releases\v$ver\ProxyChainManager.exe" diff --git a/scripts/setup_and_build.ps1 b/scripts/setup_and_build.ps1 index ec028e4..dc3e78c 100644 --- a/scripts/setup_and_build.ps1 +++ b/scripts/setup_and_build.ps1 @@ -1,75 +1,48 @@ #Requires -Version 5.1 <# .SYNOPSIS - After git pull: upgrade pip, install deps, build ProxyChainManager.exe, copy to Desktop, create "Proxy God.lnk". + Developer build: deps, tests (optional), PyInstaller, copy to Desktop, shortcut. .DESCRIPTION - Requires Python 3.10+ on PATH (python.exe) or the Windows py launcher (py -3). - Does not auto-install Python; see README for winget one-liner if needed. -#> -$ErrorActionPreference = "Stop" -$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) -Set-Location $root + Lighter than release_build.ps1 — skips SBOM/zip/manifest. For daily dev iteration. + Production releases: use scripts\release_build.ps1 or build_release.bat -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:`n" + - " winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements`n" + - "Or https://www.python.org/downloads/ (enable 'Add python.exe to PATH')." - ) -} +.PARAMETER SkipTests + Skip unittest gate (faster iteration). +#> +[CmdletBinding()] +param([switch]$SkipTests) + +$ErrorActionPreference = "Stop" +. "$PSScriptRoot\_build_common.ps1" + +$root = Get-RepoRoot +Set-Location $root $py = Resolve-PythonExe Write-Host "Using: $py" -& $py -c "import sys; assert sys.version_info >= (3, 10), 'Need Python 3.10+'; print(sys.version)" +& $py -c "import sys; assert sys.version_info >= (3, 10); print(sys.version)" -Write-Host "`n== pip (upgrade) ==" -& $py -m pip install --upgrade pip +$ver = Get-ReleaseVersion +Install-BuildDependencies -PythonExe $py -Root $root -Write-Host "`n== dependencies + PyInstaller ==" -& $py -m pip install -r "$root\requirements.txt" -# Pinned dev dep (PyInstaller version) so builds are reproducible. -& $py -m pip install -r "$root\dev-requirements.txt" - -Write-Host "`n== Stage bundled GOST binary ==" -& powershell -NoProfile -ExecutionPolicy Bypass -File "$root\scripts\prepare_bundled_gost.ps1" -if ($LASTEXITCODE -ne 0) { throw "prepare_bundled_gost.ps1 failed (exit $LASTEXITCODE)" } - -Write-Host "`n== PyInstaller (using ProxyChainManager.spec) ==" -# Always build from the spec — it bundles signup_extension/, world_map.png, -# customtkinter assets, and the right hidden imports. Do NOT override with -# CLI flags or the data files will be missing from the .exe. -$spec = Join-Path $root "ProxyChainManager.spec" -if (-not (Test-Path $spec)) { throw "Spec not found: $spec" } -& $py -m PyInstaller --noconfirm --clean $spec -if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed (exit $LASTEXITCODE)" } - -$distExe = Join-Path $root "dist\ProxyChainManager.exe" -if (-not (Test-Path $distExe)) { - throw "Build failed: missing $distExe" +if (-not $SkipTests) { + Invoke-UnitTests -PythonExe $py -Root $root } -# SHA256 sidecar so the release artifact is self-verifiable. -$distHashFile = "$distExe.sha256" -$distHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $distExe).Hash.ToLower() -"$distHash *ProxyChainManager.exe" | Out-File -FilePath $distHashFile -Encoding ascii -Force -Write-Host "SHA256: $distHash → $distHashFile" +Invoke-StageBundledGost -Root $root +Invoke-GenerateVersionInfo -PythonExe $py -Root $root -Version $ver -Commit (Get-GitCommit) + +$exe = Invoke-PyInstallerBuild -PythonExe $py -Root $root +$hashInfo = Write-ExeHashSidecar -ExePath $exe $desk = [Environment]::GetFolderPath("Desktop") $deskExe = Join-Path $desk "ProxyChainManager.exe" -Copy-Item -LiteralPath $distExe -Destination $deskExe -Force -Copy-Item -LiteralPath $distHashFile -Destination "$deskExe.sha256" -Force +Copy-Item -LiteralPath $exe -Destination $deskExe -Force +Copy-Item -LiteralPath $hashInfo.Sidecar -Destination "$deskExe.sha256" -Force Write-Host "Copied: $deskExe (+ .sha256)" Write-Host "`n== Desktop shortcut ==" -& powershell -NoProfile -ExecutionPolicy Bypass -File "$root\scripts\create_desktop_shortcut.ps1" +& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $root "scripts\create_desktop_shortcut.ps1") Write-Host "`nDone. Launch from Desktop: Proxy God.lnk (or ProxyChainManager.exe)"