# -*- mode: python ; coding: utf-8 -*- """PyInstaller onedir spec for Orisys Studio (Windows).""" from __future__ import annotations import json import os from pathlib import Path from PyInstaller.building.api import COLLECT, EXE, PYZ from PyInstaller.building.build_main import Analysis from PyInstaller.utils.hooks import collect_all, collect_data_files SPEC_DIR = Path(SPECPATH).resolve() REPO_ROOT = SPEC_DIR.parents[1] BRANDING_DIR = REPO_ROOT / "shared" / "resources" / "branding" ENTRY = SPEC_DIR / "entry.py" #: What ``setup.ps1`` recorded about this machine (interpreter, and where its runtime DLLs #: live when that interpreter is not a python.org one). BUILD_ENV_STAMP = REPO_ROOT / ".venv-build" / "orisys-build-env.json" #: The runtime DLLs an Anaconda interpreter keeps in ``\Library\bin`` or #: ``\DLLs``. PyInstaller does not search those directories, so a frozen app built #: without them dies before it can log anything: #: ``ImportError: DLL load failed while importing pyexpat`` (libexpat) #: ``ImportError: DLL load failed while importing _ctypes`` (ffi-8) #: Building with python.org CPython avoids the whole list; when ``setup.ps1`` accepts a conda #: interpreter it records the directory, and this is where that decision is honoured. _RUNTIME_DLLS = ( "libexpat.dll", "expat.dll", "ffi-8.dll", "ffi-7.dll", "ffi.dll", "libssl-3-x64.dll", "libcrypto-3-x64.dll", "sqlite3.dll", "liblzma.dll", "libbz2.dll", "zlib.dll", ) def _extra_runtime_binaries() -> list[tuple[str, str]]: """Runtime DLLs from a non-python.org interpreter, as ``(source, dest)`` pairs.""" folders: list[str] = [] try: stamp = json.loads(BUILD_ENV_STAMP.read_text(encoding="utf-8")) folders.extend(str(item) for item in (stamp.get("extra_dll_dirs") or ())) except Exception: # No stamp (a spec run by hand): fall back to the environment, if it says it is conda. prefix = os.environ.get("CONDA_PREFIX") if prefix: folders.append(os.path.join(prefix, "Library", "bin")) out: list[tuple[str, str]] = [] for folder in folders: for name in _RUNTIME_DLLS: candidate = Path(folder) / name if candidate.is_file(): out.append((str(candidate), ".")) return out def _branding_datas() -> list[tuple[str, str]]: datas: list[tuple[str, str]] = [] if not BRANDING_DIR.is_dir(): return datas for path in BRANDING_DIR.rglob("*"): if path.is_file(): datas.append((str(path), "branding")) return datas block_cipher = None # Config is copied beside the EXE by build.ps1 (writable / editable). # Branding + package data go into _internal via Analysis datas. datas = _branding_datas() binaries: list = _extra_runtime_binaries() hiddenimports = [ "cv2", "PyQt5.sip", "pyqtgraph.graphicsItems", "scipy.signal", "numba", ] datas += collect_data_files("orisys_app_shared", includes=["theme/*.json"]) try: datas += collect_data_files("orisys") except Exception: pass try: cv2_datas, cv2_binaries, cv2_hidden = collect_all("cv2") datas += cv2_datas binaries += cv2_binaries hiddenimports += list(cv2_hidden) except Exception: pass icon_path = BRANDING_DIR / "logo.ico" icon = str(icon_path) if icon_path.is_file() else None a = Analysis( [str(ENTRY)], pathex=[ str(REPO_ROOT / "apps" / "studio" / "src"), str(REPO_ROOT / "shared" / "src"), ], binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[ "open3d", # NOTE: `orisys.finger` imports `.viewer` at module level, and `finger/viewer.py` # imports open3d *inside* its functions — so bundling it costs a few KB of Python and # pulls in no Open3D. Excluding it made `import orisys.finger` raise # ModuleNotFoundError in the frozen app, which took the whole window down with it # (the app cannot even reach logging). The `open3d` entry above is the one that # matters. "tkinter", "matplotlib", "IPython", "jupyter", "notebook", "pytest", "PyQt5.QtBluetooth", "PyQt5.QtNfc", "PyQt5.QtPositioning", "PyQt5.QtLocation", "PyQt5.QtMultimedia", "PyQt5.QtMultimediaWidgets", "PyQt5.QtSensors", "PyQt5.QtSerialPort", "PyQt5.QtWebChannel", "PyQt5.QtWebEngine", "PyQt5.QtWebEngineCore", "PyQt5.QtWebEngineWidgets", "PyQt5.QtWebSockets", "PySide2", "PySide6", "PyQt6", ], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name="OrisysStudio", debug=False, bootloader_ignore_signals=False, strip=False, upx=False, console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon=icon, ) coll = COLLECT( exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=False, upx_exclude=[], name="OrisysStudio", )