266 lines
9.8 KiB
PowerShell
266 lines
9.8 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"
|
|
$InstallerDefinition = Join-Path $ProjectRoot "packaging\windows\doctor_workstation.iss"
|
|
$EnsureInstallerCompiler = Join-Path $PSScriptRoot "ensure_inno_setup.ps1"
|
|
$InstallerMessagesFile = Join-Path $ProjectRoot ".build-tools\inno-languages\ChineseSimplified.isl"
|
|
$VersionSource = Join-Path $ProjectRoot "src\doctor_workstation\__init__.py"
|
|
$MediaSmokeHook = Join-Path $ProjectRoot "packaging\runtime_media_smoke.py"
|
|
$WindowsIcon = Join-Path $ProjectRoot "resources\branding\app-icon.ico"
|
|
|
|
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"),
|
|
$VersionSource,
|
|
$MediaSmokeHook,
|
|
$WindowsIcon,
|
|
$ReleaseLauncherTemplate,
|
|
$InstallerDefinition,
|
|
$EnsureInstallerCompiler
|
|
)) {
|
|
if (-not (Test-Path -LiteralPath $RequiredFile -PathType Leaf)) {
|
|
throw "Required build file is missing: $RequiredFile"
|
|
}
|
|
}
|
|
|
|
$VersionText = [System.IO.File]::ReadAllText($VersionSource)
|
|
$VersionMatch = [regex]::Match(
|
|
$VersionText,
|
|
'(?m)^\s*__version__\s*=\s*"([^"]+)"'
|
|
)
|
|
if (-not $VersionMatch.Success) {
|
|
throw "Unable to read the application version from $VersionSource"
|
|
}
|
|
$ProjectVersion = $VersionMatch.Groups[1].Value
|
|
|
|
$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. No artifacts were generated."
|
|
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"
|
|
}
|
|
|
|
$ReleaseZip = Join-Path $DistributionRoot (
|
|
"DoctorWorkstation-Windows-x64-$ProjectVersion.zip"
|
|
)
|
|
$InstallerBaseName = "DoctorWorkstation-Setup-Windows-x64-$ProjectVersion"
|
|
$InstallerArtifact = Join-Path $DistributionRoot "$InstallerBaseName.exe"
|
|
$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"
|
|
}
|
|
|
|
if (Test-Path -LiteralPath $InstallerArtifact) {
|
|
Remove-Item -LiteralPath $InstallerArtifact -Force
|
|
}
|
|
Write-Host "Preparing the pinned Inno Setup compiler..."
|
|
$InnoCompiler = (& $EnsureInstallerCompiler | Select-Object -Last 1)
|
|
if (-not $InnoCompiler -or
|
|
-not (Test-Path -LiteralPath $InnoCompiler -PathType Leaf)) {
|
|
throw "Unable to locate the Inno Setup compiler"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $InstallerMessagesFile -PathType Leaf)) {
|
|
throw "Simplified Chinese installer messages are missing: $InstallerMessagesFile"
|
|
}
|
|
|
|
Write-Host "Creating the Windows installer..."
|
|
& $InnoCompiler `
|
|
"/DAppVersion=$ProjectVersion" `
|
|
"/DSourceDir=$Artifact" `
|
|
"/DOutputDir=$DistributionRoot" `
|
|
"/DSetupBaseName=$InstallerBaseName" `
|
|
"/DChineseMessagesFile=$InstallerMessagesFile" `
|
|
"/DAppIconFile=$WindowsIcon" `
|
|
$InstallerDefinition
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Inno Setup failed with exit code $LASTEXITCODE"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $InstallerArtifact -PathType Leaf)) {
|
|
throw "Installer compilation completed without the expected artifact: $InstallerArtifact"
|
|
}
|
|
|
|
$ReleaseHash = (Get-FileHash -LiteralPath $ReleaseZip -Algorithm SHA256).Hash
|
|
$InstallerHash = (Get-FileHash -LiteralPath $InstallerArtifact -Algorithm SHA256).Hash
|
|
$ChecksumLines = @(
|
|
"$InstallerHash $([System.IO.Path]::GetFileName($InstallerArtifact))",
|
|
"$ReleaseHash $([System.IO.Path]::GetFileName($ReleaseZip))"
|
|
)
|
|
$Utf8WithoutBom = New-Object System.Text.UTF8Encoding($false)
|
|
[System.IO.File]::WriteAllText(
|
|
$ChecksumFile,
|
|
(($ChecksumLines -join "`r`n") + "`r`n"),
|
|
$Utf8WithoutBom
|
|
)
|
|
|
|
Write-Host "Windows package complete." -ForegroundColor Green
|
|
Write-Host "Artifact: $Artifact" -ForegroundColor Green
|
|
Write-Host "Installer: $InstallerArtifact" -ForegroundColor Green
|
|
Write-Host "Release ZIP: $ReleaseZip" -ForegroundColor Green
|
|
Write-Host "Installer SHA-256: $InstallerHash" -ForegroundColor Green
|
|
Write-Host "ZIP 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
|
|
}
|