Files
kefu/wechat_rpa/build_windows_exe.ps1
T
2026-09-21 10:34:06 +08:00

203 lines
8.5 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)