c4663ef769
collect_data_files("orisys") 会把 SDK 包内受 git 跟踪的 src/orisys/.gitignore 一并拷进 _internal\orisys;
现在 spec 统一过滤版本控制与缓存文件名,build.ps1 第 6 步对我们自己发布的三个包硬失败、其它位置仅告警。
验证:清理重建 exit 0,dist 内 .git* / __pycache__ / 缓存文件 0 命中,Open3D 检查通过,冒烟测试
打印 "the application started" 与 "the packaged 3D scene loaded",输出 488.7 MB。
261 lines
8.5 KiB
RPMSpec
261 lines
8.5 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
|
|
|
|
|
|
#: Source-control and tool-cache names that must never ship. ``collect_data_files`` copies
|
|
#: *every* non-Python file a package contains, and the SDK keeps a tracked
|
|
#: ``src/orisys/.gitignore`` — which is how ``_internal\orisys\.gitignore`` reached the
|
|
#: bundle. The list is deliberately narrow: a real asset (``theme/*.json``, cv2's own
|
|
#: ``config.py``) must survive, so only VCS/cache names are dropped.
|
|
_RESIDUE_NAMES = frozenset(
|
|
{
|
|
".git",
|
|
".gitignore",
|
|
".gitattributes",
|
|
".gitmodules",
|
|
".gitkeep",
|
|
"__pycache__",
|
|
".pytest_cache",
|
|
".mypy_cache",
|
|
".ruff_cache",
|
|
".scratch",
|
|
".tox",
|
|
".idea",
|
|
".vscode",
|
|
}
|
|
)
|
|
_RESIDUE_SUFFIXES = (".pyc", ".pyo", ".orig", ".rej", "~")
|
|
|
|
|
|
def _without_residue(datas: list[tuple[str, str]]) -> list[tuple[str, str]]:
|
|
"""Drop VCS/cache residue from collected package data, naming what was dropped."""
|
|
kept: list[tuple[str, str]] = []
|
|
dropped: list[str] = []
|
|
for source, dest in datas:
|
|
parts = Path(source).parts
|
|
if any(part in _RESIDUE_NAMES for part in parts) or source.endswith(_RESIDUE_SUFFIXES):
|
|
dropped.append(source)
|
|
continue
|
|
kept.append((source, dest))
|
|
for item in dropped:
|
|
print(f"[spec] dropped residue from package data: {item}")
|
|
return kept
|
|
|
|
|
|
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",
|
|
# PySide6's runtime is shiboken6; the Qt binding itself is found from the imports, but
|
|
# QtOpenGLWidgets is unusual enough to name explicitly: it is a separate module and DLL
|
|
# (Qt6OpenGLWidgets), and without it the 3D pane reports "no OpenGL widgets" while the
|
|
# rest of the app looks fine.
|
|
"shiboken6",
|
|
"PySide6.QtOpenGLWidgets",
|
|
"pyqtgraph.graphicsItems",
|
|
"scipy.signal",
|
|
"numba",
|
|
]
|
|
datas += collect_data_files("orisys_app_shared", includes=["theme/*.json"])
|
|
try:
|
|
datas += collect_data_files("orisys")
|
|
except Exception as exc:
|
|
# Tolerated so a spec run by hand still works, but never silently: a package whose data
|
|
# cannot be collected loses assets, and that is invisible in the built EXE.
|
|
print(f"[spec] WARNING: could not collect orisys package data: {exc}")
|
|
try:
|
|
cv2_datas, cv2_binaries, cv2_hidden = collect_all("cv2")
|
|
datas += cv2_datas
|
|
binaries += cv2_binaries
|
|
hiddenimports += list(cv2_hidden)
|
|
except Exception as exc:
|
|
print(f"[spec] WARNING: could not collect cv2: {exc}")
|
|
|
|
# Applied last, so it covers every contributor above (including a third-party wheel).
|
|
datas = _without_residue(datas)
|
|
|
|
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",
|
|
# The app uses PySide6 only. Qt modules it never imports are excluded to keep the
|
|
# bundle small — but `"PySide6"` itself must NOT appear here: excluding the whole
|
|
# binding produces an EXE with no Qt at all (it fails at start-up, not at build
|
|
# time). `PySide6.QtOpenGLWidgets` is required by the 3D pane and is deliberately
|
|
# absent from this list.
|
|
"PySide6.Qt3DAnimation",
|
|
"PySide6.Qt3DCore",
|
|
"PySide6.Qt3DExtras",
|
|
"PySide6.Qt3DInput",
|
|
"PySide6.Qt3DLogic",
|
|
"PySide6.Qt3DRender",
|
|
"PySide6.QtBluetooth",
|
|
"PySide6.QtCharts",
|
|
"PySide6.QtDataVisualization",
|
|
"PySide6.QtGraphs",
|
|
"PySide6.QtHelp",
|
|
"PySide6.QtLocation",
|
|
"PySide6.QtMultimedia",
|
|
"PySide6.QtMultimediaWidgets",
|
|
"PySide6.QtNfc",
|
|
"PySide6.QtPdf",
|
|
"PySide6.QtPdfWidgets",
|
|
"PySide6.QtPositioning",
|
|
"PySide6.QtQml",
|
|
"PySide6.QtQuick",
|
|
"PySide6.QtQuick3D",
|
|
"PySide6.QtQuickControls2",
|
|
"PySide6.QtQuickWidgets",
|
|
"PySide6.QtRemoteObjects",
|
|
"PySide6.QtScxml",
|
|
"PySide6.QtSensors",
|
|
"PySide6.QtSerialBus",
|
|
"PySide6.QtSerialPort",
|
|
"PySide6.QtSpatialAudio",
|
|
"PySide6.QtStateMachine",
|
|
"PySide6.QtTextToSpeech",
|
|
"PySide6.QtWebChannel",
|
|
"PySide6.QtWebEngineCore",
|
|
"PySide6.QtWebEngineQuick",
|
|
"PySide6.QtWebEngineWidgets",
|
|
"PySide6.QtWebSockets",
|
|
"PyQt5",
|
|
"PyQt6",
|
|
"PySide2",
|
|
],
|
|
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",
|
|
)
|