official 0.1.0 version
This commit is contained in:
+235
-58
@@ -1,90 +1,134 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build Orisys Studio Windows onedir EXE with PyInstaller.
|
||||
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 when deps/spec change).
|
||||
Pass --clean to PyInstaller (slower; use after dependency or spec changes).
|
||||
|
||||
.PARAMETER SkipInstall
|
||||
Skip pip install steps (reuse existing .venv-build packages).
|
||||
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]$SkipInstall,
|
||||
[switch]$RebuildVenv,
|
||||
[switch]$SkipSmokeTest,
|
||||
[switch]$StrictSmokeTest,
|
||||
[string]$GlProfile = "desktop33"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
$StudioDir = $PSScriptRoot
|
||||
$RepoRoot = (Resolve-Path (Join-Path $StudioDir "..\..")).Path
|
||||
$VenvDir = Join-Path $RepoRoot ".venv-build"
|
||||
$Python = Join-Path $VenvDir "Scripts\python.exe"
|
||||
$Spec = Join-Path $PSScriptRoot "orisys-studio.spec"
|
||||
$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"
|
||||
|
||||
if (-not (Test-Path $Python)) {
|
||||
Write-Host "Creating build venv at $VenvDir"
|
||||
python -m venv $VenvDir
|
||||
# --------------------------------------------------------------- 1. provisioning
|
||||
if ($RebuildVenv) {
|
||||
Write-Host "Recreating the build environment ..."
|
||||
& $Setup -Force
|
||||
Assert-Ok "setup.ps1 -Force"
|
||||
}
|
||||
|
||||
if (-not $SkipInstall) {
|
||||
Write-Host "Installing build dependencies (editable shared + studio + orisys + pyinstaller)…"
|
||||
& $Python -m pip install --upgrade pip
|
||||
& $Python -m pip install "pyinstaller>=6.0"
|
||||
& $Python -m pip install -e (Join-Path $RepoRoot "shared")
|
||||
& $Python -m pip install -e (Join-Path $RepoRoot "apps\studio")
|
||||
# orisys may already be installed; ensure present without [gui]/open3d
|
||||
& $Python -m pip install "orisys>=0.5"
|
||||
|
||||
Write-Host "Forcing opencv-python-headless 4.x (orisys may have pulled opencv-python; 5.x can be broken)…"
|
||||
& $Python -m pip uninstall -y opencv-python opencv-python-headless 2>$null
|
||||
& $Python -m pip install "opencv-python-headless>=4.10.0.84,<5"
|
||||
& $Python -c "import cv2; print('cv2 OK', cv2.__version__)"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "cv2 import failed after installing opencv-python-headless"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Guard: Open3D must not be present in the build env
|
||||
$hasOpen3d = & $Python -c "import importlib.util; print(importlib.util.find_spec('open3d') is not None)"
|
||||
if ($hasOpen3d -eq "True") {
|
||||
Write-Host "Uninstalling open3d from build venv…"
|
||||
& $Python -m pip uninstall -y open3d
|
||||
}
|
||||
$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
|
||||
}
|
||||
if ($Clean) { $pyiArgs = @("--clean") + $pyiArgs }
|
||||
|
||||
Write-Host "Running PyInstaller…"
|
||||
Write-Host "Running PyInstaller ..."
|
||||
& $Python -m PyInstaller @pyiArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "PyInstaller failed with exit code $LASTEXITCODE"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
Assert-Ok "PyInstaller"
|
||||
|
||||
$OutDir = Join-Path $DistPath "OrisysStudio"
|
||||
Write-Host ""
|
||||
Write-Host "Build complete: $OutDir"
|
||||
|
||||
# Lean config beside EXE (exclude finger visualization assets)
|
||||
# -------------------------------------------------------------- 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
|
||||
}
|
||||
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
|
||||
@@ -96,21 +140,154 @@ if (Test-Path $ConfigSrc) {
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Launch: $(Join-Path $OutDir 'OrisysStudio.exe')"
|
||||
# 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)"
|
||||
}
|
||||
|
||||
# Size summary
|
||||
# ------------------------------------------------------------------- 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)
|
||||
|
||||
$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."
|
||||
}
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user