194 lines
5.6 KiB
Python
194 lines
5.6 KiB
Python
"""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() |