Build a standalone Windows app so others can run the GUI without installing
Python, uv, or any dependencies. They unzip one folder and double-click
diive-gui.exe.
From the repo root:
uv sync --extra gui --group build # one-time: install GUI + PyInstaller
.\packaging\build_gui.ps1 # build + zipTo include the 3-D surface tab (Plot ▸ 3D surface), add the gui3d extra
before building — the spec bundles VTK/PyVista only when it's present:
uv sync --extra gui --extra gui3d --group build
.\packaging\build_gui.ps1Without gui3d the app builds fine and the 3-D tab shows an install notice.
To include InfluxDB support (the Database tabs), add the db group before
building — the spec bundles influxdb-client only when present:
uv sync --extra gui --group db --group build
.\packaging\build_gui.ps1Without db the app builds fine and the Database tabs show an install notice.
Combine extras/groups freely, e.g.
uv sync --extra gui --extra gui3d --group db --group build for everything.
build_gui.ps1 automatically renders the GUI user manual (diive/gui/MANUAL.md
→ diive/gui/MANUAL.html) before PyInstaller runs, so the exe always ships a
fresh copy (Help ▸ User manual opens it). To regenerate it by hand — e.g. after
editing MANUAL.md, or to preview the HTML without doing a full build:
uv run python -m diive.gui.build_manual # or: python diive\gui\build_manual.pyThe script (diive/gui/build_manual.py) is dependency-free; MANUAL.md is the
source of truth, MANUAL.html is generated — don't hand-edit the HTML.
Output:
dist\diive-gui\— the runnable app folder (diive-gui.exe+ dependencies)dist\diive-gui-<version>+build.<stamp>-win64.zip— share this; users unzip and run the exe.<stamp>is the build timestamp (yyyyMMdd.HHmmss), so each build of the same version gets its own name, e.g.diive-gui-0.91.0+build.20260716.101500-win64.zip
Useful flags: .\packaging\build_gui.ps1 -Clean (wipe build\/dist\ first),
-NoZip (skip the archive).
Use -Clean for release builds. PyInstaller's COLLECT step overwrites but
never prunes dist\, so files from a previous build linger and get shipped —
e.g. VTK from a gui3d build you've since dropped (hundreds of MB). -Clean
guarantees the output folder matches exactly this build. (Skipping it is fine
for dev iteration.)
| File | Purpose |
|---|---|
diive_gui.spec |
PyInstaller build recipe (one-folder; bundles diive/configs data + diive/gui/MANUAL.html; collects shap/xgboost/sklearn/... ; conditionally bundles the gui3d and db optionals when installed; excludes unused Qt modules) |
launch_diive_gui.py |
Frozen-app entry point (calls diive.gui.launch) |
build_gui.ps1 |
Build + zip helper (also renders MANUAL.md → MANUAL.html first) |
make_icon.py |
Renders the splash-motif app icon to diive.ico (run once when the icon art changes) |
diive.ico |
Embedded EXE / taskbar icon (multi-size; generated by make_icon.py) |
PyInstaller's static analysis can miss dynamically-imported modules. If the exe
crashes with ModuleNotFoundError or a missing-data-file error, add the
offending package to the _collect list in diive_gui.spec (or a specific name
to hiddenimports) and rebuild. The GUI registry imports menu tabs by string on
first open, so the spec bundles all of diive.gui.tabs via
collect_submodules("diive.gui.tabs"); a new menu tab must live in that package.
The GUI runs some computes in a spawn worker process, so the entry script
(launch_diive_gui.py) calls multiprocessing.freeze_support() before importing
diive (launch() and the CLI do too). In a built exe, check that the Seasonal trend
tab opens no second window, that one extra diive-gui.exe appears while it is in
use, and that quitting removes both processes. Test by clicking through every tab and menu
once — that exercises the lazy imports.
collect_all("xgboost")aborts the build. It walksxgboost.testing, which imports test-onlyhypothesis/pytest. xgboost is collected manually instead (collect_dynamic_libs+collect_data_files+ explicithiddenimports). Don't addxgboostback to the_collectloop.Library not found: tbb12.dllduring the build is harmless — it's numba's optional Intel-TBB threading layer; numba falls back to its workqueue layer.- Bundled test suites are filtered out.
collect_allwould otherwise pull in the full test trees of numba / sklearn / statsmodels / pyarrow (thousands of*.tests.*modules — dead weight at runtime). The spec's_no_test_submodules()filter drops everytests/testing/conftestsubmodule at collection time. If a future run unexpectedly needs one, loosen that filter rather than re-adding the whole tree. - VTK/PyVista (the
gui3dextra) is collected conditionally. The spec bundlesvtkmodules/vtk/pyvista/pyvistaqt(viacollect_all) only if they're installed in the build env, so a 2-D-only build skips them. VTK routes its render window through PySide6's OpenGL modules — do not addPySide6.QtOpenGL/PySide6.QtOpenGLWidgetstoexcludes. VTK adds several hundred MB; expect a noticeably larger build with 3-D enabled.
- One-folder, not one-file — diive's scientific stack is large; a one-file exe would unpack everything to a temp dir on each launch (slow/flaky).
- Windows only — PyInstaller does not cross-compile. A macOS/Linux build must run on that OS.