Files
xuetang/app/scripts/package_windows.ps1
2026-09-08 11:40:15 +08:00

217 lines
7.6 KiB
PowerShell

[CmdletBinding()]
param(
[switch]$ValidateOnly
)
$ErrorActionPreference = "Stop"
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$BuildScript = Join-Path $PSScriptRoot "build_windows.ps1"
$BuildEnvironment = Join-Path $ProjectRoot ".venv-build"
$ProjectUvCache = Join-Path $ProjectRoot ".uv-cache"
$BuildPython = Join-Path $BuildEnvironment "Scripts\python.exe"
$FallbackPython = Join-Path $ProjectRoot ".venv\Scripts\python.exe"
$CompanionRoot = Join-Path $ProjectRoot "video_companion"
$PackageLock = Join-Path $CompanionRoot "package-lock.json"
$Artifact = Join-Path $ProjectRoot "dist\DoctorWorkstation"
$Executable = Join-Path $Artifact "DoctorWorkstation.exe"
$DistributionRoot = Join-Path $ProjectRoot "dist"
$ReleaseLauncherTemplate = Join-Path $ProjectRoot "packaging\windows\start_release.bat"
$ReleaseLauncher = Join-Path $DistributionRoot "Start_DoctorWorkstation.bat"
$ProjectMetadata = Join-Path $ProjectRoot "pyproject.toml"
$MediaSmokeHook = Join-Path $ProjectRoot "packaging\runtime_media_smoke.py"
function Test-BuildPython {
param([Parameter(Mandatory = $true)][string]$Candidate)
if (-not (Test-Path -LiteralPath $Candidate -PathType Leaf)) {
return $false
}
try {
& $Candidate -c "import sys, httpx, PyInstaller, PySide6; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)" 2>$null
return $LASTEXITCODE -eq 0
}
catch {
return $false
}
}
function Find-Application {
param([Parameter(Mandatory = $true)][string]$Name)
$Command = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($Command) { return $Command.Source }
return $null
}
try {
foreach ($RequiredFile in @(
$BuildScript,
$PackageLock,
(Join-Path $ProjectRoot "uv.lock"),
$ProjectMetadata,
$MediaSmokeHook,
$ReleaseLauncherTemplate
)) {
if (-not (Test-Path -LiteralPath $RequiredFile -PathType Leaf)) {
throw "Required build file is missing: $RequiredFile"
}
}
$Npm = Find-Application -Name "npm.cmd"
$Node = Find-Application -Name "node.exe"
if (-not $Npm -or -not $Node) {
throw "Node.js and npm are required to build the video companion."
}
$NodeVersionText = (& $Node --version).Trim().TrimStart("v")
$NodeVersion = [version]$NodeVersionText
if ($NodeVersion.Major -lt 20) {
throw "Node.js 20 or newer is required; found $NodeVersionText"
}
$Uv = Find-Application -Name "uv"
if ($ValidateOnly) {
if (-not $Uv -and
-not (Test-BuildPython -Candidate $BuildPython) -and
-not (Test-BuildPython -Candidate $FallbackPython)) {
throw "Neither uv nor a usable Python environment with build dependencies was found."
}
Write-Host "Windows package entry validation passed."
exit 0
}
if ($Uv) {
Write-Host "Preparing locked Python build dependencies in $BuildEnvironment ..."
$PreviousProjectEnvironment = [Environment]::GetEnvironmentVariable(
"UV_PROJECT_ENVIRONMENT",
"Process"
)
$PreviousUvCache = [Environment]::GetEnvironmentVariable(
"UV_CACHE_DIR",
"Process"
)
New-Item -ItemType Directory -Path $ProjectUvCache -Force | Out-Null
[Environment]::SetEnvironmentVariable(
"UV_PROJECT_ENVIRONMENT",
$BuildEnvironment,
"Process"
)
[Environment]::SetEnvironmentVariable(
"UV_CACHE_DIR",
$ProjectUvCache,
"Process"
)
Push-Location $ProjectRoot
try {
& $Uv sync --frozen --extra build
if ($LASTEXITCODE -ne 0) {
throw "uv sync for build dependencies failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
[Environment]::SetEnvironmentVariable(
"UV_PROJECT_ENVIRONMENT",
$PreviousProjectEnvironment,
"Process"
)
[Environment]::SetEnvironmentVariable(
"UV_CACHE_DIR",
$PreviousUvCache,
"Process"
)
}
}
elseif (Test-BuildPython -Candidate $BuildPython) {
Write-Host "Using existing build environment: $BuildPython"
}
elseif (Test-BuildPython -Candidate $FallbackPython) {
$BuildPython = $FallbackPython
Write-Host "Using existing project environment with build dependencies: $BuildPython"
}
else {
throw "Install uv or prepare .venv-build with the project's build dependencies."
}
if (-not (Test-BuildPython -Candidate $BuildPython)) {
throw "The prepared build Python is unusable: $BuildPython"
}
Write-Host "Installing locked video companion dependencies..."
& $Npm ci --prefix $CompanionRoot --no-audit --no-fund
if ($LASTEXITCODE -ne 0) {
throw "npm ci failed with exit code $LASTEXITCODE"
}
Write-Host "Running the existing Windows release build..."
& $BuildScript -Python $BuildPython -SkipFrontendInstall
if (-not (Test-Path -LiteralPath $Executable -PathType Leaf)) {
throw "Build completed without the expected executable: $Executable"
}
$ProjectText = [System.IO.File]::ReadAllText($ProjectMetadata)
$VersionMatch = [regex]::Match(
$ProjectText,
'(?m)^\s*version\s*=\s*"([^"]+)"'
)
if (-not $VersionMatch.Success) {
throw "Unable to read the project version from $ProjectMetadata"
}
$ProjectVersion = $VersionMatch.Groups[1].Value
$ReleaseZip = Join-Path $DistributionRoot (
"DoctorWorkstation-Windows-x64-$ProjectVersion.zip"
)
$ChecksumFile = Join-Path $DistributionRoot "SHA256SUMS.txt"
Copy-Item -LiteralPath $ReleaseLauncherTemplate -Destination $ReleaseLauncher -Force
if (Test-Path -LiteralPath $ReleaseZip) {
Remove-Item -LiteralPath $ReleaseZip -Force
}
Write-Host "Creating the distributable ZIP..."
$SevenZip = Find-Application -Name "7z.exe"
if ($SevenZip) {
Push-Location $DistributionRoot
try {
& $SevenZip a -tzip -mx=5 -mmt=on $ReleaseZip `
".\DoctorWorkstation" ".\Start_DoctorWorkstation.bat"
if ($LASTEXITCODE -ne 0) {
throw "7-Zip failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
}
else {
Compress-Archive `
-LiteralPath @($Artifact, $ReleaseLauncher) `
-DestinationPath $ReleaseZip `
-CompressionLevel Optimal
}
if (-not (Test-Path -LiteralPath $ReleaseZip -PathType Leaf)) {
throw "Packaging completed without the expected ZIP: $ReleaseZip"
}
$ReleaseHash = (Get-FileHash -LiteralPath $ReleaseZip -Algorithm SHA256).Hash
$ChecksumLine = "$ReleaseHash $([System.IO.Path]::GetFileName($ReleaseZip))`r`n"
$Utf8WithoutBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($ChecksumFile, $ChecksumLine, $Utf8WithoutBom)
Write-Host "Windows package complete." -ForegroundColor Green
Write-Host "Artifact: $Artifact" -ForegroundColor Green
Write-Host "Release ZIP: $ReleaseZip" -ForegroundColor Green
Write-Host "SHA-256: $ReleaseHash" -ForegroundColor Green
exit 0
}
catch {
$FailureExitCode = if ($LASTEXITCODE -is [int] -and $LASTEXITCODE -ne 0) {
$LASTEXITCODE
}
else {
1
}
Write-Host "Windows packaging failed." -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
exit $FailureExitCode
}