"""Build a CPU-only standalone Windows folder for Orisys 3D dashboard using PyInstaller. Usage: conda activate py310 # or a clean virtualenv with only required packages pip install pyinstaller python build/build_gui_win_pyinstaller.py Output: dist/OrisysDashboard/OrisysDashboard.exe (+ bundled Python, DLLs, config/, assets/) """ from __future__ import annotations import os import shutil import subprocess import sys from pathlib import Path # ---------- Configuration ---------- ENTRY_SCRIPT = Path("examples/finger_pipeline_3dview.py").resolve() EXE_NAME = "OrisysDashboard.exe" DIST_NAME = "OrisysDashboard" # final folder name repo_root = Path(__file__).parent.parent # Packages you want to force-include (PyInstaller often auto-detects them, but explicit avoids misses) HIDDEN_IMPORTS = [ "PyQt5", "pyqtgraph", "open3d", "cv2", "orisys", "gui", "pipeline", "viz", "gui.app_paths", "plotly", "_plotly_utils", # sometimes needed for open3d: "open3d.cpu.pybind", # adjust to actual module if needed ] # Modules to completely exclude (they won't be imported anyway if absent in venv) EXCLUDE_MODULES = [ "cupy", "cupyx", "pyopencl", "tkinter", # "unittest", # "email", # "http", # "xmlrpc", # "pkg_resources", # "setuptools", "pip", "wheel", "matplotlib", # unless you use it # "scipy", # unless you use it ] # Data directories: (source relative to repo root, destination in bundle) DATA_DIRS = [ ("config", "config"), ("assets", "assets"), ] # Optional: UPX directory (download from https://upx.github.io) UPX_DIR = r"C:\tools\upx" # set to None if not available # ---------- End Configuration ---------- def clean_dist(dist_path: Path) -> None: if dist_path.exists(): shutil.rmtree(dist_path) dist_path.mkdir(parents=True) def build_with_pyinstaller(): # Ensure we are in a clean, minimal venv print("Using Python:", sys.executable) # Output folder for PyInstaller work files work_dir = Path("build/pyinstaller_work") dist_dir = Path("dist") / DIST_NAME clean_dist(work_dir) clean_dist(dist_dir) # Build PyInstaller command cmd = [ sys.executable, "-m", "PyInstaller", "--onedir", # produce a folder, not a single exe "--name", DIST_NAME, f"--distpath={dist_dir.parent}", f"--workpath={work_dir}", f"--specpath=build", "--noconfirm", "--clean", "--windowed", # or "--console" if you need terminal ] # Add data files for src_rel, dest_rel in DATA_DIRS: src = Path(src_rel).resolve() if not src.is_dir(): raise FileNotFoundError(f"Data directory missing: {src}") cmd.append(f"--add-data={src};{dest_rel}") # Logo file if exists logo = Path("logo.png") if logo.exists(): cmd.append(f"--add-data={logo.resolve()};.") # Hidden imports for mod in HIDDEN_IMPORTS: cmd.append(f"--hidden-import={mod}") # Exclude modules for mod in EXCLUDE_MODULES: cmd.append(f"--exclude-module={mod}") # UPX if available if UPX_DIR and Path(UPX_DIR).is_dir(): cmd.append(f"--upx-dir={UPX_DIR}") # Entry point cmd.append(str(ENTRY_SCRIPT)) # Environment (same as Nuitka script) env = os.environ.copy() env["PYTHONPATH"] = str(Path("src").resolve()) + os.pathsep + str(Path("examples").resolve()) + os.pathsep + env.get("PYTHONPATH", "") print("\n=== Running PyInstaller ===") print("Command:", " ".join(cmd)) subprocess.check_call(cmd, env=env) # After build, copy data directories explicitly # PyInstaller output folder dist_dir = Path("dist") / DIST_NAME final_exe = dist_dir / EXE_NAME if not final_exe.is_file(): raise RuntimeError(f"Executable not found at {final_exe}") print("\n=== Copying data directories ===") for src_rel, dest_rel in DATA_DIRS: src = repo_root / src_rel # use an absolute reference to repo root dest = dist_dir / dest_rel if not src.is_dir(): raise FileNotFoundError(f"Data directory not found: {src}") if dest.exists(): shutil.rmtree(dest) shutil.copytree(src, dest, dirs_exist_ok=True) # Python 3.8+ use dirs_exist_ok=True print(f" {src} -> {dest}") # Copy logo logo_src = repo_root / "logo.png" if logo_src.is_file(): shutil.copy2(logo_src, dist_dir / "logo.png") print(f" Logo copied") # Write README (same as Nuitka script) readme = dist_dir / "README.txt" readme.write_text( "\n".join([ "Orisys Finger Dashboard (standalone)", "", "Run:", f" {EXE_NAME}", f" {EXE_NAME} --video path\\to\\video.mp4", f" {EXE_NAME} --config config\\dd02-ov.json", "", "Bundled folders:", " config/ sensor JSON configs", " assets/ finger mesh and visualization data", "", "CPU-only build (no CUDA/CuPy).", ]), encoding="utf-8", ) print(f"\n[SUCCESS] Standalone GUI folder: {dist_dir}") print(f"Executable: {final_exe}") # Optional smoke test (same as Nuitka script) try: from verify import verify_gui_dist print("\n=== Smoke test ===") verify_gui_dist(dist_dir) except Exception as exc: print(f"\n[WARNING] Smoke test failed: {exc}") return dist_dir if __name__ == "__main__": build_with_pyinstaller()