collect_data_files("orisys") 会把 SDK 包内受 git 跟踪的 src/orisys/.gitignore 一并拷进 _internal\orisys;
现在 spec 统一过滤版本控制与缓存文件名,build.ps1 第 6 步对我们自己发布的三个包硬失败、其它位置仅告警。
验证:清理重建 exit 0,dist 内 .git* / __pycache__ / 缓存文件 0 命中,Open3D 检查通过,冒烟测试
打印 "the application started" 与 "the packaged 3D scene loaded",输出 488.7 MB。
Build
Windows packaging for the studio desktop app: one provisioning step per machine, then repeatable builds.
AutoCal is not built here.
apps/autocal(the internal 5-axis platform control app) runs from source and has no PyInstaller spec, deliberately: this spec would otherwise have to learn about the ZMotion runtime DLLs, and AutoCal must stay out of the customer installer. Seeapps/autocal/README.md.
Layout
build/
studio/
setup.bat / setup.ps1 # provision the build venv (run once per machine)
constraints-build.txt # pinned build-time versions
build.bat / build.ps1 # build the EXE and verify it
entry.py # frozen entry (no source chdir)
orisys-studio.spec # PyInstaller onedir spec
_pyinstaller/ # PyInstaller workpath (gitignored)
_smoke/ # log of the last build's start-up smoke test
dist/
OrisysStudio/
OrisysStudio.exe
config/ # lean locales + sensors (no finger visualization/)
_internal/ # Python, PySide6, opencv-headless, orisys, OpenGL, …
Why there are two steps
A PyInstaller bundle depends on the machine as much as on the sources: which interpreter,
whether it is Anaconda (whose libexpat/libffi/OpenSSL DLLs live outside the venv and are
therefore not bundled), and where the orisys SDK checkout is. Those facts are decided
once, by setup.ps1, and recorded in .venv-build\orisys-build-env.json.
build.ps1 then installs nothing and guesses nothing: it verifies the environment, builds,
and verifies the artefact. That split is what keeps a build reproducible instead of
"works on my machine".
First time on a machine
cd build\studio
setup.bat
setup.bat -Python C:\Python310\python.exe
setup.bat -AllowConda
setup.bat finds an interpreter, creates or repairs .venv-build, installs the pinned
toolchain plus the app's extras (including the studio's three-d extra — without it the 3D
pane silently reports "PyOpenGL is not installed" on the customer's machine), writes the
stamp, and then verifies the result by importing what the frozen app imports.
Interpreter resolution, in order: -Python <path>; py -3.10; python; the interpreter
already behind .venv-build. The Microsoft Store python.exe alias is refused (it is not an
interpreter), and an Anaconda interpreter is refused unless -AllowConda is passed — in that
case its Library\bin is recorded and the spec adds the runtime DLLs, because PyInstaller
does not search there and the EXE would otherwise die at start-up with
ImportError: DLL load failed while importing pyexpat. Building with python.org CPython is
still the preferred answer.
The SDK is installed from ..\..\..\orisys-sdk when that checkout exists (-SdkPath to point
elsewhere, -FromPypi to install the published wheel instead). Installing the published SDK
is how a build once shipped with no orisys.finger at all.
Re-running setup.bat is safe: with a valid stamp for this checkout it reports and exits;
without one it repairs the existing venv rather than deleting it. -Force recreates it.
Build
build.bat
build.bat -Clean # after dependency/spec changes
build.bat -RebuildVenv # setup.bat -Force, then build
build.bat -SkipInstall # never provision: fail instead
build.bat -SkipSmokeTest # do not launch the produced EXE
build.bat -StrictSmokeTest # treat a 3D-context warning as a failure
Or from the repo root:
powershell -ExecutionPolicy Bypass -File .\build\studio\build.ps1
build.bat always resolves build.ps1 next to itself (cwd-independent) and forwards flags.
What the build verifies, and why
Every silent failure this packaging has produced looked like a successful build, so the script checks the things that only show up when the EXE runs:
| check | failure it prevents |
|---|---|
the venv imports orisys_app_shared, orisys_studio, orisys, OpenGL, cv2, PyInstaller |
a stale editable install still builds (the spec's pathex finds the modules) and produces an EXE that cannot start |
collect_data_files('orisys_app_shared', includes=['theme/*.json']) is non-empty |
the theme colours are package data; without them the app dies in build_qss() before logging anything |
every config\sensors\**\*.oryscene is present, and the finger manifest/sensor.json/unroll map survive the config strip |
the 3D pane refuses the scene when the device description cannot be derived |
no open3d* under dist |
the offline tool leaking into the shipped bundle |
no source-control or cache files in the bundle (_without_residue in the spec, step 6 of the script) |
a .gitignore, __pycache__ or .scratch from the source trees shipping inside dist — the SDK keeps a .gitignore inside the orisys package, so collect_data_files('orisys') copies it |
smoke test: start the produced EXE, wait for its own log to say Studio shell started (and 3D scene loaded when a finger scene shipped), then stop it |
a bundle that cannot start at all — missing runtime DLLs, an excluded module, missing data. The log is kept under build\_smoke\ |
The smoke test seeds a preference.json beside the EXE to select the finger sensor with the
3D pane on, and restores (or removes) it afterwards. A GL context failure is reported as a
warning, because it is environment-dependent; -StrictSmokeTest turns it into a failure.
Where the app's own code lives in the bundle
_internal\ contains native code and data files only. Every pure-Python module —
orisys_studio.*, orisys_app_shared.*, orisys.* — is compiled into the PYZ archive that
sits inside OrisysStudio.exe (that is most of the EXE's ~13 MB). So there is deliberately
no _internal\orisys_studio\ folder, and no .py files anywhere: PyInstaller ships bytecode,
never sources.
_internal therefore shows only these orisys-related paths:
_internal\orisys_app_shared\theme\colors_dark.json <- package data, bundled (qss.py reads it)
_internal\orisys_app_shared\theme\colors_light.json
_internal\branding\... <- logos
(collect_data_files("orisys") exists for the case where the SDK is installed from a wheel that
bundles sensors/ assets. With a local editable checkout its only non-.py file was the SDK's
own src\orisys\.gitignore — a repository file with no business inside an importable package — so
it landed here as _internal\orisys\.gitignore. The spec now filters source-control and cache
files out of every datas entry, and step 6 of the script fails the build if any survive in a
package we ship.)
To list what is really in the archive:
.venv-build\Scripts\python.exe -m PyInstaller.utils.cliutils.archive_viewer ^
-l -r -b dist\OrisysStudio\OrisysStudio.exe
(The PYZ-00.toc under build\_pyinstaller\orisys-studio\ is the same information at build
time, and build\_smoke\*.log is the runtime proof: "Studio shell started" and "3D scene
loaded" can only be printed if those modules imported.)
Dependency notes
- PySide6 is the Qt binding (LGPLv3). PyQt5 was replaced because its free option is
GPLv3-or-commercial; the apps are PySide6-only and their tests enforce it
(
apps/studio/tests/test_binding_purity.py,apps/autocal/tests/test_binding_purity.py).setup.ps1uninstalls PyQt5/PyQt6/PySide2 from the build venv and verifies only one binding is present:pyqtgraphbinds to whichever toolkit it imports first, so a leftover PyQt5 yields a bundle whose plots were built from the wrong binding. The spec also excludesPyQt5/PyQt6/PySide2outright, and must never excludePySide6itself. - App shared dep uses
opencv-python-headless(PySide6 owns the GUI).setup.ps1uninstalls anyopencv-pythonand pinsopencv-python-headless>=4.10,<5(5.x has shipped wheels without acv2module) and verifiesimport cv2. - PyOpenGL is the GL binding for the 3D pane and comes from the studio's
three-dextra. Add theprotected-assetsextra (cryptography) if sealed.oryscenepackages are shipped. QOpenGLWidgetlives inPySide6.QtOpenGLWidgets, notQtWidgets(Qt6 moved it): the spec names it inhiddenimportsso the module andQt6OpenGLWidgets.dllare bundled.- Open3D stays excluded (spec + build env): it is an offline geometry tool. The 3D pane is
Qt
QOpenGLWidget+ PyOpenGL. constraints-build.txtpins the toolchain. Move a pin only together withsetup.bat -Forceand a passingbuild.bat— the smoke test is the acceptance evidence, not the file.
GL note
The pane asks for the profile in gl_profile (desktop33 by default, gles30 otherwise). A
desktop33 request needs a desktop GL 3.3 core context; on some sessions Qt falls back to its
ANGLE (GLES) backend, which cannot satisfy it, and the widget then reports no context on a
machine whose GPU is fine. QT_OPENGL=desktop is the workaround; the durable fix is to pin
the backend in orisys_studio.main before QApplication is constructed
(Qt.AA_UseDesktopOpenGL / Qt.AA_UseOpenGLES). build.bat -StrictSmokeTest makes this a
build failure.
Size / startup
- onedir (not onefile): faster cold start; ship the whole
OrisysStudiofolder. - Finger
assets/visualization/is stripped from the shippedconfig/(~11 MB saved). Nothing in the app reads it (manifest.json'sviz_poseis used only by the SDK examples); the 3D pane needsscene/*.orysceneplusassets/30fps-0703, and the build checks both. - Studio shell defers OpenCV / acquisition imports until Preview or Run.
Expect ~450 MB total (Qt + numpy/scipy/numba + OpenCV + PyOpenGL).
Verification on a new machine
setup.bat(see its report: interpreter, conda or not, SDK source).build.bat— its smoke test must print "the application started" and "the packaged 3D scene loaded".- Launch
OrisysStudio.exeand check theme/locale/prefs;preference.jsonandoutputs\appear next to the EXE. - Camera preview + box/mini/finger Run (video) with image modes and force plots.
- Open the 3D pane on a finger source: the surface draws, the legend shows the field.
- Confirm no
open3d*files underdist\OrisysStudio, and no.gitignore/__pycache__/.scratchanywhere inside it (step 6 of the build fails on residue in our own packages).
Editable run (unchanged)
$env:PYTHONPATH = "D:\repo\orisys-app\shared\src;D:\repo\orisys-app\apps\studio\src"
python D:\repo\orisys-app\apps\studio\run.py