203 lines
8.5 KiB
PowerShell
203 lines
8.5 KiB
PowerShell
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)
|