246 lines
11 KiB
PowerShell
246 lines
11 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"
|
|
$WindowsIcon = Join-Path $ProjectRoot "resources\branding\app-icon.ico"
|
|
|
|
function Invoke-FrozenGate {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Executable,
|
|
[Parameter(Mandatory = $true)][string]$Argument,
|
|
[Parameter(Mandatory = $true)][string]$GateName
|
|
)
|
|
|
|
$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 = $Argument
|
|
$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 "$GateName timed out after 30 seconds`n$Diagnostics"
|
|
}
|
|
if ($Process.ExitCode -ne 0) {
|
|
throw "$GateName 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 "$GateName logs contain an unhandled exception"
|
|
}
|
|
Write-Host "$GateName passed ($Argument, isolated offscreen mode)."
|
|
}
|
|
finally {
|
|
foreach ($Name in $Environment.Keys) {
|
|
[Environment]::SetEnvironmentVariable($Name, $PreviousEnvironment[$Name], "Process")
|
|
}
|
|
if (Test-Path -LiteralPath $SmokeRoot) {
|
|
Remove-Item -LiteralPath $SmokeRoot -Recurse -Force
|
|
}
|
|
}
|
|
}
|
|
|
|
function Assert-FrozenMultimedia {
|
|
param([Parameter(Mandatory = $true)][string]$Artifact)
|
|
|
|
foreach ($RequiredName in @(
|
|
"QtMultimedia.pyd",
|
|
"QtMultimediaWidgets.pyd",
|
|
"Qt6Multimedia.dll",
|
|
"Qt6MultimediaWidgets.dll"
|
|
)) {
|
|
$RequiredFile = Get-ChildItem -LiteralPath $Artifact -Recurse -Filter $RequiredName -File |
|
|
Where-Object { $_.FullName -like "*\PySide6\*" } |
|
|
Select-Object -First 1
|
|
if (-not $RequiredFile) {
|
|
throw "Frozen Qt multimedia component is missing: $RequiredName"
|
|
}
|
|
}
|
|
|
|
$PluginDirectories = @(Get-ChildItem -LiteralPath $Artifact -Recurse -Directory |
|
|
Where-Object { $_.Name -eq "multimedia" -and $_.Parent.Name -eq "plugins" })
|
|
if ($PluginDirectories.Count -eq 0) {
|
|
throw "Frozen PySide6 plugins/multimedia directory is missing"
|
|
}
|
|
foreach ($BackendName in @("ffmpegmediaplugin.dll", "windowsmediaplugin.dll")) {
|
|
$Backend = $PluginDirectories |
|
|
ForEach-Object {
|
|
Get-ChildItem -LiteralPath $_.FullName -Filter $BackendName -File -ErrorAction SilentlyContinue
|
|
} |
|
|
Select-Object -First 1
|
|
if (-not $Backend) {
|
|
throw "Frozen Qt multimedia backend is missing: $BackendName"
|
|
}
|
|
}
|
|
|
|
Write-Host "Frozen Qt multimedia file gate passed."
|
|
}
|
|
|
|
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"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $WindowsIcon -PathType Leaf)) {
|
|
throw "Windows application icon was not found: $WindowsIcon"
|
|
}
|
|
|
|
$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" }
|
|
|
|
$BuildPythonBase = (& $Python -c "import sys; print(sys.base_prefix)").Trim()
|
|
if ($LASTEXITCODE -ne 0 -or -not $BuildPythonBase) {
|
|
throw "Unable to resolve the build Python runtime directory"
|
|
}
|
|
# Dependency scanning must not collect unrelated ICU/OpenSSL libraries from
|
|
# an editor's helper tools (for example Poppler) ahead of Windows libraries.
|
|
$PreviousBuildPath = $env:PATH
|
|
$BuildRuntimePaths = @(
|
|
(Split-Path -Parent $Python),
|
|
$BuildPythonBase,
|
|
(Join-Path $BuildPythonBase "DLLs"),
|
|
(Join-Path $env:SystemRoot "System32"),
|
|
$env:SystemRoot,
|
|
(Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0")
|
|
)
|
|
try {
|
|
$env:PATH = ($BuildRuntimePaths | Select-Object -Unique) -join [System.IO.Path]::PathSeparator
|
|
& $Python -m PyInstaller --noconfirm --clean $Spec
|
|
$PyInstallerExitCode = $LASTEXITCODE
|
|
}
|
|
finally {
|
|
$env:PATH = $PreviousBuildPath
|
|
}
|
|
if ($PyInstallerExitCode -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"
|
|
}
|
|
Assert-FrozenMultimedia -Artifact $Artifact
|
|
Invoke-FrozenGate `
|
|
-Executable $Executable `
|
|
-Argument "--media-smoke-test" `
|
|
-GateName "Frozen Qt multimedia smoke gate"
|
|
Invoke-FrozenGate `
|
|
-Executable $Executable `
|
|
-Argument "--smoke-test" `
|
|
-GateName "Frozen application entry smoke gate"
|
|
|
|
Write-Host "Build complete: $Artifact"
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|