param( [string]$PythonPath = "" ) $ErrorActionPreference = "Stop" $projectRoot = [System.IO.Path]::GetFullPath($PSScriptRoot) $buildEnvironment = Join-Path $projectRoot ".build-venv" $buildPython = Join-Path $buildEnvironment "Scripts\python.exe" $requirements = Join-Path $projectRoot "requirements.txt" $spec = Join-Path $projectRoot "wechat_rpa.spec" $versionLine = Select-String -LiteralPath (Join-Path $projectRoot "app_version.py") -Pattern '^APP_VERSION\s*=\s*"([^"]+)"$' if (-not $versionLine) { throw "Cannot read APP_VERSION from app_version.py" } $version = $versionLine.Matches[0].Groups[1].Value $env:WECOM_BUILD_VERSION = $version $env:UV_CACHE_DIR = Join-Path $projectRoot ".uv-cache" function Test-BuildPython([string]$Candidate) { if (-not $Candidate -or -not (Test-Path -LiteralPath $Candidate)) { return $false } try { & $Candidate -c "import ssl, _ssl, PyInstaller, PySide6, requests, numpy, PIL, pyautogui, pyperclip, win32gui, mcp, rapidocr_onnxruntime, Crypto.Cipher.AES" *> $null return $LASTEXITCODE -eq 0 } catch { # Windows PowerShell 5 treats native stderr as a terminating error with # ErrorActionPreference=Stop, even when redirected. Try the next runtime. return $false } } $candidates = @() if ($PythonPath) { $candidates += [System.IO.Path]::GetFullPath($PythonPath) } $projectPython = Join-Path $projectRoot ".venv\Scripts\python.exe" if (Test-Path -LiteralPath $projectPython) { $candidates += $projectPython } if (Test-Path -LiteralPath $buildPython) { $candidates += $buildPython } try { $launcherPython = (& py -3.11 -c "import sys; print(sys.executable)" 2>$null | Select-Object -Last 1).Trim() if ($launcherPython) { $candidates += $launcherPython } } catch {} try { # 部分 Windows 机器只安装了 python.exe,没有 Python Launcher(py.exe)。 # 这种环境原本会误判为“没有 Python”并转而要求 uv,实际运行环境明明可用。 $pathPython = (& python -c "import sys; print(sys.executable)" 2>$null | Select-Object -Last 1).Trim() if ($pathPython) { $candidates += $pathPython } } catch {} $python = $candidates | Where-Object { Test-BuildPython $_ } | Select-Object -First 1 if (-not $python) { $uv = Get-Command uv -ErrorAction SilentlyContinue if ($uv) { if (-not (Test-Path -LiteralPath $buildPython)) { & $uv.Source venv --python 3.12 $buildEnvironment if ($LASTEXITCODE -ne 0) { throw "Failed to create the EXE build environment" } } & $uv.Source pip install --python $buildPython -r $requirements pyinstaller if ($LASTEXITCODE -ne 0) { throw "Failed to install EXE build dependencies" } } else { $bootstrapPython = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if (-not $bootstrapPython) { throw "No Python found. Install Python 3.11/3.12 or provide -PythonPath pointing to python.exe." } if (-not (Test-Path -LiteralPath $buildPython)) { & $bootstrapPython -m venv $buildEnvironment if ($LASTEXITCODE -ne 0) { throw "Failed to create the EXE build environment" } } & $buildPython -m ensurepip --upgrade if ($LASTEXITCODE -ne 0) { throw "Failed to prepare pip in the build environment" } & $buildPython -m pip install -r $requirements pyinstaller if ($LASTEXITCODE -ne 0) { throw "Failed to install EXE build dependencies" } } if (-not (Test-BuildPython $buildPython)) { throw "Build dependencies could not be imported" } $python = $buildPython } Write-Output ("Build Python: " + $python) Push-Location $projectRoot try { & $python -m PyInstaller --noconfirm --clean $spec if ($LASTEXITCODE -ne 0) { throw "PyInstaller build failed" } } finally { Pop-Location } $appName = "ZhenAI-WeCom-Assistant-v$version" $appDir = Join-Path $projectRoot "dist\$appName" $exe = Join-Path $appDir "$appName.exe" if (-not (Test-Path -LiteralPath $exe)) { throw ("Build completed but EXE was not found: " + $exe) } $internalDir = Join-Path $appDir "_internal" # Qt 与其他视觉库可能分别携带不同版本的 VC Runtime。应用根目录优先级 # 最高,因此明确放置与当前 PySide6/Qt 完全配套的一组,避免 PATH 中第三方 # 工具的 DLL 在打包分析时抢占同名文件。 $pysideRuntimeDir = (& $python -c "import pathlib, PySide6; print(pathlib.Path(PySide6.__file__).resolve().parent)" | Select-Object -Last 1).Trim() foreach ($runtimeName in @( "MSVCP140.dll", "MSVCP140_1.dll", "MSVCP140_2.dll", "VCRUNTIME140.dll", "VCRUNTIME140_1.dll" )) { $runtimeSource = Join-Path $pysideRuntimeDir $runtimeName if (-not (Test-Path -LiteralPath $runtimeSource)) { throw ("PySide6 runtime file is missing: " + $runtimeName) } Copy-Item -LiteralPath $runtimeSource -Destination (Join-Path $internalDir $runtimeName) -Force } $requiredRuntimeFiles = @( "PySide6\QtWebEngineProcess.exe", "PySide6\resources\qtwebengine_resources.pak", "PySide6\resources\icudtl.dat", "PySide6\translations\qtwebengine_locales\zh-CN.pak", "MSVCP140.dll", "MSVCP140_1.dll", "MSVCP140_2.dll", "VCRUNTIME140.dll", "VCRUNTIME140_1.dll" ) foreach ($requiredFile in $requiredRuntimeFiles) { if (-not (Test-Path -LiteralPath (Join-Path $internalDir $requiredFile))) { throw ("Required embedded runtime file is missing: " + $requiredFile) } } Write-Output "Embedded browser and VC runtime files verified" # 自动升级必须由安装目录之外的独立进程执行,否则 Windows 会阻止正在运行的 # 主程序覆盖自身。更新器只依赖标准库,单独打成 onefile,主程序启动它前会再 # 复制到 updates 目录,因此 NSIS 可以安全替换安装目录中的这一份。 $updaterDist = Join-Path $projectRoot "dist\updater-helper" $updaterWork = Join-Path $projectRoot "build\updater-helper" New-Item -ItemType Directory -Path $updaterWork -Force | Out-Null & $python -m PyInstaller --noconfirm --clean --onefile --name ZhenAIUpdater ` --distpath $updaterDist --workpath $updaterWork --specpath $updaterWork ` (Join-Path $projectRoot "updater_main.py") if ($LASTEXITCODE -ne 0) { throw "Updater EXE build failed" } $updaterExe = Join-Path $updaterDist "ZhenAIUpdater.exe" if (-not (Test-Path -LiteralPath $updaterExe)) { throw "Updater EXE was not found" } & (Join-Path $projectRoot "sign_artifact.ps1") -Path $updaterExe if ($LASTEXITCODE -ne 0) { throw "Updater code signing step failed" } Copy-Item -LiteralPath $updaterExe -Destination (Join-Path $appDir "ZhenAIUpdater.exe") -Force Write-Output "Independent updater built and bundled" # 有证书时给主程序签名(无证书自动跳过)。签名要在自检前完成, # 保证自检跑的就是最终分发的那个文件。 & (Join-Path $projectRoot "sign_artifact.ps1") -Path $exe if ($LASTEXITCODE -ne 0) { throw "Code signing step failed" } # 模拟没有开发工具 PATH 的目标电脑,防止漏包 DLL 被构建机意外补齐。 $buildProcessPath = $env:PATH try { $env:PATH = (Join-Path $env:SystemRoot "System32") + ";" + $env:SystemRoot $selfCheck = Start-Process -FilePath $exe -ArgumentList "--packaging-self-check" -WorkingDirectory $appDir -PassThru -WindowStyle Hidden } finally { $env:PATH = $buildProcessPath } try { if (-not $selfCheck.WaitForExit(90000)) { $selfCheck.Kill() throw "Packaged Qt WebEngine self-check timed out" } if ($selfCheck.ExitCode -ne 0) { throw ("Packaged Qt WebEngine self-check failed with exit code " + $selfCheck.ExitCode) } } finally { if ($selfCheck -and -not $selfCheck.HasExited) { $selfCheck.Kill() } } Write-Output "Packaged Qt WebEngine self-check passed" Write-Output "Packaged real HTTPS and certificate verification checks passed (clean PATH)" Write-Output "Packaged automatic decryption and incremental cloud upload checks passed (synthetic data, mocked transport)" Write-Output "Packaged Messages navigation and OCR checks passed (synthetic UI, mocked input)" Write-Output "Packaged cross-machine capture, DPI and unread click checks passed (synthetic pixels, mocked input)" Write-Output "Packaged self-reply feedback and send guards passed (synthetic archive, mocked input)" Write-Output "Packaged background database detection and database-first reply checks passed (synthetic data, mocked input)" $hash = (Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash Write-Output ("App folder: " + $appDir) Write-Output ("EXE: " + $exe) Write-Output ("SHA256: " + $hash)