#Requires -Version 5.1 <# .SYNOPSIS Build the Orisys Studio Windows onedir EXE with PyInstaller. .DESCRIPTION This script installs nothing. Provisioning belongs to `setup.ps1`, which decides the machine-specific facts once (interpreter, Anaconda or not, where the SDK checkout is) and records them in `.venv-build\orisys-build-env.json`. Here we only: 1. make sure that provisioning happened (and run `setup.ps1` if it plainly has not), 2. verify the environment can import what the frozen app imports, 3. build, 4. verify the artefact — config, excludes, and a real start-up of the produced EXE. Step 2 and step 4 exist because the interesting failures are silent ones: a stale venv still builds (the spec's pathex finds the modules) yet loses the theme data; an excluded module still builds yet makes `import orisys.finger` fail; a missing runtime DLL still builds yet the EXE never writes a log. Each of those looked like a successful build. .PARAMETER Clean Pass --clean to PyInstaller (slower; use after dependency or spec changes). .PARAMETER SkipInstall Do not run `setup.ps1`, even if the environment looks unprovisioned. Fails instead of provisioning silently. .PARAMETER RebuildVenv Run `setup.ps1 -Force` first (recreate the venv from scratch), then build. .PARAMETER SkipSmokeTest Do not launch the produced EXE. The smoke test is the only step that proves the bundle starts; skip it only where a desktop session is genuinely unavailable. .PARAMETER StrictSmokeTest Treat a 3D-context warning from the smoke test as a failure. .PARAMETER GlProfile GL profile the smoke test starts the app with (default: the shipped `desktop33`). #> param( [switch]$Clean, [switch]$SkipInstall, [switch]$RebuildVenv, [switch]$SkipSmokeTest, [switch]$StrictSmokeTest, [string]$GlProfile = "desktop33" ) $ErrorActionPreference = "Stop" $StudioDir = $PSScriptRoot $RepoRoot = (Resolve-Path (Join-Path $StudioDir "..\..")).Path $VenvDir = Join-Path $RepoRoot ".venv-build" $Python = Join-Path $VenvDir "Scripts\python.exe" $StampPath = Join-Path $VenvDir "orisys-build-env.json" $Spec = Join-Path $StudioDir "orisys-studio.spec" $Setup = Join-Path $StudioDir "setup.ps1" $WorkPath = Join-Path $RepoRoot "build\_pyinstaller" $DistPath = Join-Path $RepoRoot "dist" $OutDir = Join-Path $DistPath "OrisysStudio" $Exe = Join-Path $OutDir "OrisysStudio.exe" $SmokeKeep = Join-Path $RepoRoot "build\_smoke" function Assert-Ok([string]$what) { if ($LASTEXITCODE -ne 0) { throw "$what failed (exit $LASTEXITCODE)" } } # PowerShell 5.1's `Set-Content -Encoding UTF8` writes a BOM, and a BOM makes Python's # `json.loads` raise ("Unexpected UTF-8 BOM") — the app then silently falls back to its # defaults, which is exactly the kind of quiet failure this script exists to avoid. function Write-JsonFile([string]$path, [string]$json) { [System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding($false))) } Write-Host "Repo: $RepoRoot" # --------------------------------------------------------------- 1. provisioning if ($RebuildVenv) { Write-Host "Recreating the build environment ..." & $Setup -Force Assert-Ok "setup.ps1 -Force" } $stampRoot = "" if (Test-Path $StampPath) { $stampRoot = (Get-Content $StampPath -Raw | ConvertFrom-Json).checkout_root } $needsSetup = (-not (Test-Path $Python)) -or ($stampRoot -ne $RepoRoot) if ($needsSetup) { if ($SkipInstall) { throw "the build environment is missing or was provisioned for another checkout. Run setup.bat (or drop -SkipInstall)." } Write-Host "No usable build environment for this checkout: running setup.ps1 ..." & $Setup Assert-Ok "setup.ps1" } if (-not (Test-Path $Spec)) { throw "spec not found: $Spec" } # ---------------------------------------------------- 2. environment verification # Cheap (about a second) and it catches the two provisioning mistakes that build silently: # an editable install pointing at another checkout, and a missing GL binding. Write-Host "Verifying the build environment ..." & $Python -c "import orisys_app_shared, orisys_studio, orisys, OpenGL, cv2, PyInstaller; print('imports OK')" Assert-Ok "the build venv cannot import what the app imports" & $Python -c "from PyInstaller.utils.hooks import collect_data_files as c; d=c('orisys_app_shared', includes=['theme/*.json']); assert d, 'theme data not collectable'; print('theme data OK:', len(d), 'files')" Assert-Ok "theme data would not be bundled" # ------------------------------------------------------------------ 3. PyInstaller $pyiArgs = @( "--noconfirm", "--distpath", $DistPath, "--workpath", $WorkPath, $Spec ) if ($Clean) { $pyiArgs = @("--clean") + $pyiArgs } Write-Host "Running PyInstaller ..." & $Python -m PyInstaller @pyiArgs Assert-Ok "PyInstaller" Write-Host "" Write-Host "Build complete: $OutDir" # -------------------------------------------------------------- 4. shipped config # Lean config beside the EXE (the finger visualization bundle is a development artefact). $ConfigSrc = Join-Path $RepoRoot "apps\studio\config" $ConfigDst = Join-Path $OutDir "config" if (Test-Path $ConfigSrc) { Write-Host "Copying lean config to $ConfigDst ..." if (Test-Path $ConfigDst) { Remove-Item $ConfigDst -Recurse -Force } New-Item -ItemType Directory -Path $ConfigDst -Force | Out-Null Get-ChildItem $ConfigSrc -Force | ForEach-Object { Copy-Item $_.FullName -Destination $ConfigDst -Recurse -Force } $viz = Join-Path $ConfigDst "sensors\finger\assets\visualization" if (Test-Path $viz) { Remove-Item $viz -Recurse -Force Write-Host "Excluded sensors/finger/assets/visualization" } } # The 3D pane refuses the whole scene when the device description cannot be derived, and that # description needs the sensor manifest, the unroll map and the compiled package. A strip # step that removed one of them would leave a pane that explains itself instead of drawing. Write-Host "Checking the shipped 3D prerequisites ..." $scenePackages = @(Get-ChildItem (Join-Path $ConfigDst "sensors") -Recurse -Filter "*.oryscene" -ErrorAction SilentlyContinue) if ($scenePackages.Count -eq 0) { Write-Warning "No *.oryscene package under config\sensors: the 3D pane will report 'no compiled scene package'." } else { foreach ($pkg in $scenePackages) { Write-Host " scene: $($pkg.FullName.Substring($ConfigDst.Length + 1))" } } $fingerCfg = Join-Path $ConfigDst "sensors\finger" if (Test-Path $fingerCfg) { $required = @("manifest.json", "sensor.json") foreach ($name in $required) { if (-not (Test-Path (Join-Path $fingerCfg $name))) { throw "the shipped finger config is missing $name" } } $manifest = Get-Content (Join-Path $fingerCfg "manifest.json") -Raw | ConvertFrom-Json $assets = Join-Path $fingerCfg ([string]$manifest.assets_dir) $sensor = Get-Content (Join-Path $fingerCfg "sensor.json") -Raw | ConvertFrom-Json $scale = $sensor.camera.source_scale $mapName = if ($null -eq $scale -or [double]$scale -eq 1.0) { "unroll_map_uv.npz" } else { "unroll_map_uv_s$([double]$scale).npz" } if (-not (Test-Path (Join-Path $assets $mapName))) { throw "the shipped finger assets have no $mapName (the device geometry description needs it; the pane would refuse the scene)" } Write-Host " finger assets: $($manifest.assets_dir) ($mapName present)" } # ------------------------------------------------------------------- 5. no Open3D $o3dHits = Get-ChildItem $OutDir -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'open3d' } if ($o3dHits) { Write-Warning "Open3D-related files found under dist (should be empty):" $o3dHits | ForEach-Object { Write-Warning $_.FullName } } else { Write-Host "Open3D check: none found under dist." } # ----------------------------------------------------------------- 6. smoke test # Start the EXE the way a user would and require the application's own log to say it came up. # This is the step that would have caught every silent failure found in this build. $smokeFailed = $false $smokeWarned = $false if (-not $SkipSmokeTest -and (Test-Path $Exe)) { Write-Host "" Write-Host "Smoke test: starting $Exe ..." $prefPath = Join-Path $OutDir "preference.json" $hadPref = Test-Path $prefPath $prefBackup = $null if ($hadPref) { $prefBackup = Get-Content $prefPath -Raw } $logsDir = Join-Path $OutDir "outputs\logs" $before = @() $outputsExisted = Test-Path (Join-Path $OutDir "outputs") if (Test-Path $logsDir) { $before = @(Get-ChildItem $logsDir -Filter *.log | ForEach-Object { $_.Name }) } # The pane is shown for the finger sensor only; seeding the preference is how the build # proves the *packaged* asset loads, not just that the window opened. $sensorFor3D = "" if (Test-Path $fingerCfg) { $sensorFor3D = "finger" } $seed = [ordered]@{ language = "en_US"; theme = "dark"; view_preset = "live"; show_finger_3d = $true } if ($sensorFor3D) { $seed["sensor_type"] = $sensorFor3D } ($seed | ConvertTo-Json) | ForEach-Object { Write-JsonFile $prefPath $_ } $oldQpa = $env:QT_QPA_PLATFORM $env:QT_QPA_PLATFORM = "windows" $proc = $null try { $proc = Start-Process -FilePath $Exe -WorkingDirectory $OutDir -PassThru # The scene is read on a worker thread *after* the shell is up, so the two lines # arrive separately: waiting for both (rather than reading the log once) is what # keeps this test from being a race. $wanted = @("Studio shell started") if ($sensorFor3D) { $wanted += "3D scene loaded" } $deadline = (Get-Date).AddSeconds(75) $logText = "" $logPath = "" while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 1000 $fresh = Get-ChildItem $logsDir -Filter *.log -ErrorAction SilentlyContinue | Where-Object { $before -notcontains $_.Name } if ($fresh) { $newest = $fresh | Sort-Object LastWriteTime | Select-Object -Last 1 $logPath = $newest.FullName $logText = Get-Content $logPath -Raw -ErrorAction SilentlyContinue $missing = @($wanted | Where-Object { $logText -notmatch [regex]::Escape($_) }) if ($missing.Count -eq 0) { break } } if ($proc.HasExited) { break } } if (-not $logText -or $logText -notmatch "Studio shell started") { $smokeFailed = $true Write-Warning "The produced EXE did not reach 'Studio shell started'." if ($logPath) { Write-Warning "log: $logPath" } else { Write-Warning "no log was written at all" } } else { Write-Host " the application started" if ($sensorFor3D -and $logText -notmatch "3D scene loaded") { $smokeFailed = $true Write-Warning "The 3D scene did not load from the shipped package." # The loader logs the diagnostic it refused with; quoting it here is the # difference between "the build failed" and "this asset was rejected". $rejection = @($logText -split "`r?`n" | Where-Object { $_ -match "rejected|no compiled scene|asset_" }) if ($rejection) { Write-Warning (" " + $rejection[-1]) } } elseif ($sensorFor3D) { Write-Host " the packaged 3D scene loaded" } # A fallback to Qt's ANGLE backend cannot satisfy a desktop 3.3 core request, and # the pane then reports itself unavailable on a machine whose GPU is fine. It is # environment-dependent, so it is a warning unless -StrictSmokeTest is asked for. if ($logText -match "QWindowsEGLContext|Failed to create context|context creation failed") { $smokeWarned = $true Write-Warning "The 3D widget could not create a GL context in this session." Write-Warning " desktop33: set QT_OPENGL=desktop (Qt otherwise falls back to ANGLE, which is GLES-only)" Write-Warning " or pin it in the app (Qt.AA_UseDesktopOpenGL / AA_UseOpenGLES) before QApplication." } } if ($logPath) { New-Item -ItemType Directory -Path $SmokeKeep -Force | Out-Null $keepName = "smoke-{0}.log" -f (Get-Date).ToString("yyyyMMdd_HHmmss") Copy-Item $logPath (Join-Path $SmokeKeep $keepName) -Force Write-Host " smoke log kept: build\_smoke\$keepName" } } finally { if ($proc -and -not $proc.HasExited) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } # The app is not given the chance to save preferences; restore what was there. if ($hadPref) { Write-JsonFile $prefPath $prefBackup } elseif (Test-Path $prefPath) { Remove-Item $prefPath -Force -ErrorAction SilentlyContinue } if (-not $outputsExisted -and (Test-Path (Join-Path $OutDir "outputs"))) { Remove-Item (Join-Path $OutDir "outputs") -Recurse -Force -ErrorAction SilentlyContinue } if ($null -eq $oldQpa) { Remove-Item Env:\QT_QPA_PLATFORM -ErrorAction SilentlyContinue } else { $env:QT_QPA_PLATFORM = $oldQpa } } } # ------------------------------------------------------------------- 7. summary if (Test-Path $OutDir) { $bytes = (Get-ChildItem $OutDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum $mb = [math]::Round($bytes / 1MB, 1) Write-Host ("Output size: {0} MB" -f $mb) Write-Host "Launch: $Exe" } if ($smokeFailed -or ($StrictSmokeTest -and $smokeWarned)) { Write-Host "" Write-Host "FAILED: the build produced an EXE that did not pass the smoke test." -ForegroundColor Red exit 1 } exit 0