293 lines
13 KiB
PowerShell
293 lines
13 KiB
PowerShell
#Requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Provision the Windows packaging environment for Orisys Studio. Run once per machine.
|
|
|
|
.DESCRIPTION
|
|
Building the EXE has two kinds of requirement, and mixing them is what makes a build
|
|
"work on my machine":
|
|
|
|
* things that are true of *this machine* — which interpreter exists, whether it is
|
|
Anaconda (whose libexpat/libffi DLLs live outside the venv and are therefore not
|
|
bundled), where the orisys SDK checkout is. Those are decided **here**, once, and
|
|
recorded in `.venv-build\orisys-build-env.json`.
|
|
* things that are true of the *sources* — the specs, the excludes, the assets. Those
|
|
belong to `build.ps1`, which then installs nothing and guesses nothing.
|
|
|
|
So this script is the only place that installs anything. It creates `.venv-build` (or
|
|
repairs it), installs the pinned toolchain plus the app's own extras, writes the stamp that
|
|
`build.ps1` and the spec read, and verifies the result by importing what the frozen app
|
|
imports before anything is packaged.
|
|
|
|
Re-running is safe: with a valid stamp and no -Force it reports and exits; without a stamp
|
|
it repairs the existing venv instead of deleting it.
|
|
|
|
.PARAMETER Python
|
|
Interpreter used to create the venv. Default: `py -3.10` when available, else `python`,
|
|
else the interpreter that already backs `.venv-build`.
|
|
|
|
.PARAMETER SdkPath
|
|
Local `orisys` SDK checkout to install editable. Default: `..\..\..\orisys-sdk` when that
|
|
exists. Without it (or with -FromPypi) the SDK comes from PyPI, which is *not* the code
|
|
under test — that is how a build once shipped with no `orisys.finger` at all.
|
|
|
|
.PARAMETER Force
|
|
Recreate the venv from scratch.
|
|
|
|
.PARAMETER AllowConda
|
|
Accept an Anaconda interpreter. Anaconda keeps libexpat/libffi/OpenSSL/SQLite in
|
|
`<prefix>\Library\bin`, where PyInstaller does not look; the stamp records that directory
|
|
and the spec adds those DLLs. Building with a python.org CPython is still preferred.
|
|
|
|
.PARAMETER FromPypi
|
|
Install `orisys` from PyPI instead of a local checkout.
|
|
|
|
.PARAMETER NoVerify
|
|
Skip the post-install verification (not recommended; that verification is what catches a
|
|
provisioning which would produce an EXE that cannot start).
|
|
|
|
.EXAMPLE
|
|
setup.bat
|
|
setup.bat -Force
|
|
setup.bat -Python C:\Python310\python.exe
|
|
setup.bat -AllowConda
|
|
#>
|
|
param(
|
|
[string]$Python = "",
|
|
[string]$SdkPath = "",
|
|
[switch]$Force,
|
|
[switch]$AllowConda,
|
|
[switch]$FromPypi,
|
|
[switch]$NoVerify
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$StudioDir = $PSScriptRoot
|
|
$RepoRoot = (Resolve-Path (Join-Path $StudioDir "..\..")).Path
|
|
$VenvDir = Join-Path $RepoRoot ".venv-build"
|
|
$VenvPy = Join-Path $VenvDir "Scripts\python.exe"
|
|
$VenvCfg = Join-Path $VenvDir "pyvenv.cfg"
|
|
$StampPath = Join-Path $VenvDir "orisys-build-env.json"
|
|
$Constraints = Join-Path $StudioDir "constraints-build.txt"
|
|
#: Bumped when the stamp's meaning changes: an old stamp must not be read as valid.
|
|
$StampVersion = 1
|
|
#: `pyinstaller` is pinned here as well as in the constraints file: it is the tool, not a
|
|
#: dependency of the app, and its version decides what the bundle contains.
|
|
$PyInstallerPin = "pyinstaller==6.22.2"
|
|
#: opencv-python 5.x has shipped wheels without a `cv2` module; the app is tested on 4.x.
|
|
$CvPin = "opencv-python-headless>=4.10.0.84,<5"
|
|
|
|
# A condition the operator has to fix, with the instructions in the message itself.
|
|
function Stop-With([string]$message) {
|
|
Write-Host ""
|
|
Write-Host $message -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# 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 spec reads this stamp, so a BOM here would
|
|
# quietly drop the extra runtime DLLs and the EXE would not start.
|
|
function Write-JsonFile([string]$path, [string]$json) {
|
|
[System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding($false)))
|
|
}
|
|
|
|
# A path that is really a Python: the Microsoft Store alias on PATH answers `python` but is
|
|
# not an interpreter, and it would otherwise be silently chosen.
|
|
function Test-BasePython([string]$exe) {
|
|
if (-not $exe) { return $null }
|
|
if ($exe -match '\\WindowsApps\\') { return $null }
|
|
if (-not (Test-Path $exe)) { return $null }
|
|
try {
|
|
$out = & $exe -c "import sys; print(sys.executable)" 2>$null
|
|
if ($LASTEXITCODE -ne 0 -or -not $out) { return $null }
|
|
return ($out | Select-Object -Last 1).Trim()
|
|
} catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Resolve-BasePython([string]$explicit) {
|
|
if ($explicit) {
|
|
$ok = Test-BasePython $explicit
|
|
if (-not $ok) { Stop-With "-Python is not a usable interpreter: $explicit" }
|
|
return $ok
|
|
}
|
|
$tried = @()
|
|
if (Get-Command py.exe -ErrorAction SilentlyContinue) {
|
|
$out = & py -3.10 -c "import sys; print(sys.executable)" 2>$null
|
|
$tried += "py -3.10"
|
|
if ($LASTEXITCODE -eq 0 -and $out) {
|
|
$ok = Test-BasePython (($out | Select-Object -Last 1).Trim())
|
|
if ($ok) { return $ok }
|
|
}
|
|
}
|
|
foreach ($name in @("python3.10.exe", "python.exe")) {
|
|
$found = Get-Command $name -ErrorAction SilentlyContinue
|
|
if (-not $found) { continue }
|
|
$tried += $found.Source
|
|
$ok = Test-BasePython $found.Source
|
|
if ($ok) { return $ok }
|
|
}
|
|
# Last resort: whatever already backs this venv. An existing build environment is
|
|
# evidence of an interpreter that works on this machine.
|
|
if (Test-Path $VenvCfg) {
|
|
$match = Select-String -Path $VenvCfg -Pattern '^home\s*=\s*(.+)$'
|
|
if ($match) {
|
|
$baseHome = $match.Matches[0].Groups[1].Value.Trim()
|
|
$tried += "$baseHome (from pyvenv.cfg)"
|
|
$ok = Test-BasePython (Join-Path $baseHome "python.exe")
|
|
if ($ok) { return $ok }
|
|
}
|
|
}
|
|
Stop-With "no usable Python found (tried: $($tried -join ', ')). Install CPython 3.10+ and pass -Python <path>."
|
|
}
|
|
|
|
function Get-PythonFacts([string]$exe) {
|
|
$json = & $exe -c "import json,os,sys;print(json.dumps({'exe':sys.executable,'version':sys.version.split()[0],'prefix':sys.prefix,'conda':os.path.isdir(os.path.join(sys.prefix,'conda-meta')) or 'conda' in sys.prefix.lower()}))"
|
|
if ($LASTEXITCODE -ne 0) { throw "could not query $exe" }
|
|
return ($json | Select-Object -Last 1 | ConvertFrom-Json)
|
|
}
|
|
|
|
Write-Host "Orisys Studio build environment"
|
|
Write-Host " repo : $RepoRoot"
|
|
Write-Host " venv : $VenvDir"
|
|
|
|
# --------------------------------------------------------------------- stamp check
|
|
if ((Test-Path $VenvPy) -and (Test-Path $StampPath) -and -not $Force) {
|
|
$stamp = Get-Content $StampPath -Raw | ConvertFrom-Json
|
|
if ($stamp.stamp_version -eq $StampVersion -and $stamp.checkout_root -eq $RepoRoot) {
|
|
Write-Host ""
|
|
Write-Host "Already provisioned ($($stamp.created)); nothing to do."
|
|
Write-Host " base python : $($stamp.base_python) ($($stamp.python_version))"
|
|
Write-Host " sdk : $($stamp.sdk)"
|
|
Write-Host "Use -Force to rebuild the venv, or go straight to ..\studio\build.bat"
|
|
exit 0
|
|
}
|
|
Write-Host "The existing venv was provisioned for another checkout or an older stamp: repairing it."
|
|
}
|
|
|
|
# ------------------------------------------------------------------- interpreter
|
|
$BasePy = Resolve-BasePython $Python
|
|
$facts = Get-PythonFacts $BasePy
|
|
Write-Host " base python : $($facts.exe) ($($facts.version))"
|
|
|
|
$extraDllDirs = @()
|
|
if ($facts.conda) {
|
|
$libraryBin = Join-Path $facts.prefix "Library\bin"
|
|
if (-not $AllowConda) {
|
|
Stop-With @"
|
|
$($facts.exe) is an Anaconda interpreter.
|
|
|
|
PyInstaller does not search Anaconda's Library\bin, where libexpat.dll, ffi-8.dll,
|
|
libssl/libcrypto and sqlite3.dll live, so the EXE it produces fails at start-up with
|
|
"ImportError: DLL load failed while importing pyexpat". Either:
|
|
|
|
* build with python.org CPython (recommended): setup.bat -Python C:\Python310\python.exe
|
|
* or accept the risk: setup.bat -AllowConda
|
|
|
|
-AllowConda records $libraryBin in the build stamp and the spec adds those DLLs.
|
|
"@
|
|
}
|
|
if (Test-Path $libraryBin) {
|
|
$extraDllDirs += $libraryBin
|
|
Write-Host " conda : accepted; runtime DLLs will be taken from $libraryBin"
|
|
} else {
|
|
Write-Warning "conda prefix $($facts.prefix) has no Library\bin; the EXE may not start"
|
|
}
|
|
}
|
|
|
|
# ----------------------------------------------------------------------- the venv
|
|
if ($Force -and (Test-Path $VenvDir)) {
|
|
Write-Host "Removing $VenvDir"
|
|
Remove-Item $VenvDir -Recurse -Force
|
|
}
|
|
if (-not (Test-Path $VenvPy)) {
|
|
Write-Host "Creating venv with $BasePy"
|
|
& $BasePy -m venv $VenvDir
|
|
if ($LASTEXITCODE -ne 0) { throw "python -m venv failed" }
|
|
} else {
|
|
Write-Host "Reusing the existing venv (use -Force to recreate it)"
|
|
}
|
|
|
|
# ------------------------------------------------------------------------ the SDK
|
|
$sdkSource = ""
|
|
if (-not $FromPypi) {
|
|
$candidate = if ($SdkPath) { $SdkPath } else { Join-Path (Split-Path $RepoRoot -Parent) "orisys-sdk" }
|
|
if ($candidate -and (Test-Path (Join-Path $candidate "pyproject.toml"))) {
|
|
$sdkSource = (Resolve-Path $candidate).Path
|
|
} elseif ($SdkPath) {
|
|
throw "-SdkPath has no pyproject.toml: $SdkPath"
|
|
}
|
|
}
|
|
if (-not $sdkSource) {
|
|
Write-Warning "No local orisys SDK checkout found; installing 'orisys>=0.5' from PyPI. The EXE will not contain the SDK under test unless that is what you intend."
|
|
}
|
|
|
|
# ----------------------------------------------------------------------- install
|
|
Write-Host "Installing the pinned toolchain and the app's own packages ..."
|
|
$install = @("-m", "pip", "install", "--disable-pip-version-check")
|
|
if (Test-Path $Constraints) { $install += @("-c", $Constraints) }
|
|
if ($sdkSource) { $install += @("-e", $sdkSource) } else { $install += "orisys>=0.5" }
|
|
$install += @("-e", (Join-Path $RepoRoot "shared"))
|
|
# The `three-d` extra is the GL binding. Without it the app still builds, and the 3D pane
|
|
# silently reports "PyOpenGL is not installed" on the customer's machine.
|
|
$install += @("-e", "$(Join-Path $RepoRoot 'apps\studio')[three-d]")
|
|
$install += @($PyInstallerPin, $CvPin)
|
|
& $VenvPy @install
|
|
if ($LASTEXITCODE -ne 0) { throw "pip install failed" }
|
|
|
|
# Upstream `orisys` may declare opencv-python; the app needs the headless wheel (PyQt owns
|
|
# the GUI), and 5.x can arrive without a `cv2` module.
|
|
& $VenvPy -m pip uninstall -y opencv-python opencv-python-headless 2>$null | Out-Null
|
|
& $VenvPy -m pip install --disable-pip-version-check -c $Constraints $CvPin
|
|
if ($LASTEXITCODE -ne 0) { throw "opencv-python-headless install failed" }
|
|
|
|
# Open3D must not be in the build env: it is an offline tool, and the spec excludes it.
|
|
$hasOpen3d = & $VenvPy -c "import importlib.util; print(importlib.util.find_spec('open3d') is not None)"
|
|
if ($hasOpen3d -eq "True") {
|
|
Write-Host "Uninstalling open3d from the build venv ..."
|
|
& $VenvPy -m pip uninstall -y open3d
|
|
}
|
|
|
|
# ------------------------------------------------------------------- verification
|
|
if (-not $NoVerify) {
|
|
Write-Host ""
|
|
Write-Host "Verifying the provisioned environment ..."
|
|
# These are the imports the frozen app makes before it can draw anything. Checking them
|
|
# here is what turns "the build succeeded" into "the build can start".
|
|
& $VenvPy -c "import orisys_app_shared, orisys_studio, orisys, OpenGL, cv2, PyQt5.QtWidgets, PyInstaller; print('imports OK')"
|
|
if ($LASTEXITCODE -ne 0) { throw "the venv cannot import what the app imports (see above)" }
|
|
# Theme colours are package data: PyInstaller only bundles them if the package is
|
|
# importable at build time, so a stale editable install loses them silently.
|
|
& $VenvPy -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')"
|
|
if ($LASTEXITCODE -ne 0) { throw "theme data would not be bundled" }
|
|
# When a local checkout was asked for, the installed SDK must be that checkout.
|
|
if ($sdkSource) {
|
|
& $VenvPy -c "import orisys, os; p=os.path.dirname(orisys.__file__); assert os.path.isfile(os.path.join(p,'finger','__init__.py')), 'orisys.finger missing from '+p; print('SDK OK:', p)"
|
|
if ($LASTEXITCODE -ne 0) { throw "the installed SDK has no finger package" }
|
|
}
|
|
}
|
|
|
|
# ------------------------------------------------------------------------ stamp
|
|
$stampData = [ordered]@{
|
|
stamp_version = $StampVersion
|
|
checkout_root = $RepoRoot
|
|
created = (Get-Date).ToString("o")
|
|
base_python = $facts.exe
|
|
python_version = $facts.version
|
|
is_conda = [bool]$facts.conda
|
|
conda_prefix = $facts.prefix
|
|
extra_dll_dirs = @($extraDllDirs)
|
|
sdk = $sdkSource
|
|
venv = $VenvDir
|
|
}
|
|
Write-JsonFile $StampPath ($stampData | ConvertTo-Json -Depth 4)
|
|
Write-Host "Wrote $StampPath"
|
|
|
|
Write-Host ""
|
|
Write-Host "Environment ready. Next:"
|
|
Write-Host " build.bat # build the EXE"
|
|
Write-Host " build.bat -Clean # after dependency/spec changes"
|
|
Write-Host " build.bat -SkipSmokeTest # skip launching the produced EXE"
|