Files
zyt/app/scripts/build_windows.ps1
T
2026-08-10 17:29:05 +08:00

172 lines
7.9 KiB
PowerShell

[CmdletBinding()]
param(
[string]$Python = ".venv-build\Scripts\python.exe",
[switch]$SkipFrontendInstall
)
$ErrorActionPreference = "Stop"
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$CompanionRoot = Join-Path $ProjectRoot "video_companion"
$Spec = Join-Path $ProjectRoot "packaging\doctor_workstation.spec"
function Invoke-FrozenSmokeTest {
param([Parameter(Mandatory = $true)][string]$Executable)
$TempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
$SmokeRoot = Join-Path $TempRoot ("doctor-workstation-smoke-" + [guid]::NewGuid().ToString("N"))
$SmokeRoot = [System.IO.Path]::GetFullPath($SmokeRoot)
if (-not ($SmokeRoot.StartsWith($TempRoot, [System.StringComparison]::OrdinalIgnoreCase)) -or
-not ([System.IO.Path]::GetFileName($SmokeRoot)).StartsWith("doctor-workstation-smoke-")) {
throw "Refusing to use an unsafe smoke-test directory: $SmokeRoot"
}
$Environment = @{
"APPDATA" = (Join-Path $SmokeRoot "AppData\Roaming")
"LOCALAPPDATA" = (Join-Path $SmokeRoot "AppData\Local")
"XDG_CONFIG_HOME" = (Join-Path $SmokeRoot "xdg\config")
"XDG_STATE_HOME" = (Join-Path $SmokeRoot "xdg\state")
"XDG_CACHE_HOME" = (Join-Path $SmokeRoot "xdg\cache")
"DOCTOR_CONFIG_DIR" = (Join-Path $SmokeRoot "doctor\config")
"DOCTOR_LOG_DIR" = (Join-Path $SmokeRoot "doctor\logs")
"DOCTOR_API_BASE_URL" = "https://127.0.0.1:9"
"DOCTOR_DEMO_MODE" = "true"
"DOCTOR_VIDEO_MODE" = "embedded"
"DOCTOR_VIDEO_WEB_URL" = ""
"DOCTOR_VERIFY_SSL" = "true"
"DOCTOR_LOG_LEVEL" = "INFO"
"DOCTOR_SMOKE_TEST" = "1"
"HTTP_PROXY" = "http://127.0.0.1:9"
"HTTPS_PROXY" = "http://127.0.0.1:9"
"ALL_PROXY" = "http://127.0.0.1:9"
"NO_PROXY" = ""
"QT_QPA_PLATFORM" = "offscreen"
}
$PreviousEnvironment = @{}
$StandardOutput = Join-Path $SmokeRoot "stdout.txt"
$StandardError = Join-Path $SmokeRoot "stderr.txt"
New-Item -ItemType Directory -Path $SmokeRoot -Force | Out-Null
New-Item -ItemType Directory -Path $Environment["APPDATA"] -Force | Out-Null
New-Item -ItemType Directory -Path $Environment["LOCALAPPDATA"] -Force | Out-Null
try {
foreach ($Name in $Environment.Keys) {
$PreviousEnvironment[$Name] = [Environment]::GetEnvironmentVariable($Name, "Process")
[Environment]::SetEnvironmentVariable($Name, $Environment[$Name], "Process")
}
# Windows PowerShell 5 can leave ExitCode unset on the object returned
# by Start-Process. System.Diagnostics.Process retains the real code
# and lets us drain redirected streams without risking a pipe deadlock.
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
$ProcessInfo.FileName = $Executable
$ProcessInfo.Arguments = "--smoke-test"
$ProcessInfo.UseShellExecute = $false
$ProcessInfo.CreateNoWindow = $true
$ProcessInfo.RedirectStandardOutput = $true
$ProcessInfo.RedirectStandardError = $true
$Process = New-Object System.Diagnostics.Process
$Process.StartInfo = $ProcessInfo
if (-not $Process.Start()) {
throw "Unable to start the frozen application smoke test"
}
$OutputTask = $Process.StandardOutput.ReadToEndAsync()
$ErrorTask = $Process.StandardError.ReadToEndAsync()
$TimedOut = -not $Process.WaitForExit(30000)
if ($TimedOut -and -not $Process.HasExited) {
$Process.Kill()
}
$Process.WaitForExit()
$OutputTask.Wait()
$ErrorTask.Wait()
Set-Content -LiteralPath $StandardOutput -Value $OutputTask.Result -Encoding UTF8
Set-Content -LiteralPath $StandardError -Value $ErrorTask.Result -Encoding UTF8
$DiagnosticFiles = @($StandardOutput, $StandardError)
$DiagnosticFiles += Get-ChildItem -LiteralPath $SmokeRoot -Recurse -Filter "*.log" -File |
Select-Object -ExpandProperty FullName
$Diagnostics = ($DiagnosticFiles | Where-Object { Test-Path -LiteralPath $_ } |
ForEach-Object { Get-Content -LiteralPath $_ -Raw -ErrorAction SilentlyContinue }) -join "`n"
if ($TimedOut) {
throw "Frozen application smoke test timed out after 30 seconds`n$Diagnostics"
}
if ($Process.ExitCode -ne 0) {
throw "Frozen application smoke test failed with exit code $($Process.ExitCode)`n$Diagnostics"
}
if ($Diagnostics -match "(?im)traceback \(most recent call last\)|unhandled exception|uncaught exception|fatal python error") {
throw "Frozen application smoke-test logs contain an unhandled exception"
}
Write-Host "Frozen entry smoke test passed (--smoke-test, isolated demo mode)."
}
finally {
foreach ($Name in $Environment.Keys) {
[Environment]::SetEnvironmentVariable($Name, $PreviousEnvironment[$Name], "Process")
}
if (Test-Path -LiteralPath $SmokeRoot) {
Remove-Item -LiteralPath $SmokeRoot -Recurse -Force
}
}
}
if (-not [System.IO.Path]::IsPathRooted($Python)) {
$Python = Join-Path $ProjectRoot $Python
}
if (-not (Test-Path -LiteralPath $Python -PathType Leaf)) {
throw "Build Python was not found: $Python"
}
$Npm = (Get-Command npm.cmd -ErrorAction Stop).Source
Push-Location $ProjectRoot
try {
if (-not $SkipFrontendInstall) {
& $Npm ci --prefix $CompanionRoot --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { throw "npm ci failed" }
}
& $Npm run build --prefix $CompanionRoot
if ($LASTEXITCODE -ne 0) { throw "video companion build failed" }
& $Python -m PyInstaller --noconfirm --clean $Spec
if ($LASTEXITCODE -ne 0) { throw "PyInstaller build failed" }
$Artifact = Join-Path $ProjectRoot "dist\DoctorWorkstation"
$Helper = Get-ChildItem -LiteralPath $Artifact -Recurse -Filter "QtWebEngineProcess.exe" -File | Select-Object -First 1
$Resources = Get-ChildItem -LiteralPath $Artifact -Recurse -Filter "qtwebengine_resources*.pak" -File | Select-Object -First 1
$Companion = Get-ChildItem -LiteralPath $Artifact -Recurse -Filter "index.html" -File |
Where-Object { $_.FullName -like "*video_companion_dist*" } |
Select-Object -First 1
if (-not $Helper) { throw "QtWebEngineProcess.exe is missing from the artifact" }
if (-not $Resources) { throw "QtWebEngine Chromium resources are missing from the artifact" }
if (-not $Companion) { throw "video_companion_dist is missing from the artifact" }
$PythonBase = (& $Python -c "import sys; print(sys.base_prefix)").Trim()
if ($LASTEXITCODE -ne 0 -or -not $PythonBase) {
throw "Unable to resolve the build Python runtime directory"
}
foreach ($RuntimeDllName in @("libssl-3-x64.dll", "libcrypto-3-x64.dll")) {
$SourceRuntimeDll = Join-Path $PythonBase "DLLs\$RuntimeDllName"
$FrozenRuntimeDll = Join-Path $Artifact "_internal\$RuntimeDllName"
if (-not (Test-Path -LiteralPath $SourceRuntimeDll -PathType Leaf)) {
throw "Build Python runtime dependency is missing: $SourceRuntimeDll"
}
if (-not (Test-Path -LiteralPath $FrozenRuntimeDll -PathType Leaf)) {
throw "Frozen Python runtime dependency is missing: $FrozenRuntimeDll"
}
$SourceHash = (Get-FileHash -LiteralPath $SourceRuntimeDll -Algorithm SHA256).Hash
$FrozenHash = (Get-FileHash -LiteralPath $FrozenRuntimeDll -Algorithm SHA256).Hash
if ($SourceHash -ne $FrozenHash) {
throw "Frozen $RuntimeDllName does not match the build Python runtime"
}
}
$Executable = Join-Path $Artifact "DoctorWorkstation.exe"
if (-not (Test-Path -LiteralPath $Executable -PathType Leaf)) {
throw "Frozen application entry point is missing: $Executable"
}
Invoke-FrozenSmokeTest -Executable $Executable
Write-Host "Build complete: $Artifact"
}
finally {
Pop-Location
}