Files
orisys-app/build/studio/orisys-studio.spec
T
2026-09-16 09:43:57 +08:00

183 lines
5.3 KiB
RPMSpec

# -*- 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 ``<prefix>\Library\bin`` or
#: ``<prefix>\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",
)