173 lines
6.4 KiB
RPMSpec
173 lines
6.4 KiB
RPMSpec
# -*- mode: python ; coding: utf-8 -*-
|
|
"""Cross-platform PyInstaller onedir spec for the video-enabled workstation.
|
|
|
|
PyInstaller's official PySide6 QtWebEngine hooks are activated by the explicit
|
|
hidden imports below. Those hooks retain QtWebEngineProcess, Chromium .pak/
|
|
ICU resources, locales, Qt plugins, and the macOS framework/helper layout.
|
|
The explicit QtMultimedia imports likewise activate PyInstaller's official Qt
|
|
dependency scan, which collects the QtMultimedia/QtMultimediaWidgets extension
|
|
modules, Qt6Multimedia shared libraries/frameworks, multimedia plugins, and
|
|
their platform media backends.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
PROJECT_ROOT = Path(SPEC).resolve().parent.parent
|
|
SOURCE_ROOT = PROJECT_ROOT / "src"
|
|
ENTRY_POINT = SOURCE_ROOT / "doctor_workstation" / "__main__.py"
|
|
VIDEO_DIST = PROJECT_ROOT / "video_companion" / "dist"
|
|
RESOURCES = PROJECT_ROOT / "resources"
|
|
WINDOWS_ICON = RESOURCES / "branding" / "app-icon.ico"
|
|
MACOS_ICON = RESOURCES / "branding" / "app-icon.icns"
|
|
ENTITLEMENTS = PROJECT_ROOT / "packaging" / "macos" / "entitlements.plist"
|
|
VERSION_SOURCE = SOURCE_ROOT / "doctor_workstation" / "__init__.py"
|
|
VERSION_TEMPLATE = PROJECT_ROOT / "packaging" / "windows" / "version_info.template.txt"
|
|
MEDIA_SMOKE_HOOK = PROJECT_ROOT / "packaging" / "runtime_media_smoke.py"
|
|
|
|
|
|
def read_application_version():
|
|
source = VERSION_SOURCE.read_text(encoding="utf-8")
|
|
match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', source, re.MULTILINE)
|
|
if not match:
|
|
raise SystemExit(f"Application version is missing from {VERSION_SOURCE}")
|
|
return match.group(1)
|
|
|
|
|
|
def windows_version_tuple(version):
|
|
match = re.match(r"^(\d+(?:\.\d+){0,3})", version)
|
|
if not match:
|
|
raise SystemExit(f"Application version is invalid for Windows resources: {version}")
|
|
parts = [int(part) for part in match.group(1).split(".")]
|
|
return tuple((parts + [0, 0, 0, 0])[:4])
|
|
|
|
|
|
def generate_windows_version_file(version):
|
|
template = VERSION_TEMPLATE.read_text(encoding="utf-8")
|
|
version_tuple = ", ".join(str(part) for part in windows_version_tuple(version))
|
|
rendered = template.replace("@VERSION_TUPLE@", version_tuple)
|
|
rendered = rendered.replace("@VERSION_STRING@", version)
|
|
output = PROJECT_ROOT / "build" / "generated" / "version_info.txt"
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(rendered, encoding="utf-8")
|
|
return output
|
|
|
|
|
|
APP_VERSION = read_application_version()
|
|
if sys.platform == "win32":
|
|
if not VERSION_TEMPLATE.is_file():
|
|
raise SystemExit(f"Windows version template is missing: {VERSION_TEMPLATE}")
|
|
VERSION_FILE = generate_windows_version_file(APP_VERSION)
|
|
else:
|
|
VERSION_FILE = None
|
|
|
|
if not ENTRY_POINT.is_file():
|
|
raise SystemExit(f"Application entry point is missing: {ENTRY_POINT}")
|
|
if not (VIDEO_DIST / "index.html").is_file():
|
|
raise SystemExit("Build video_companion before running PyInstaller")
|
|
if not MEDIA_SMOKE_HOOK.is_file():
|
|
raise SystemExit(f"Frozen multimedia smoke hook is missing: {MEDIA_SMOKE_HOOK}")
|
|
if sys.platform == "win32" and not WINDOWS_ICON.is_file():
|
|
raise SystemExit(f"Windows application icon is missing: {WINDOWS_ICON}")
|
|
if sys.platform == "darwin" and not MACOS_ICON.is_file():
|
|
raise SystemExit(f"macOS application icon is missing: {MACOS_ICON}")
|
|
|
|
# Some Windows developer tools add an unrelated OpenSSL installation to PATH.
|
|
# PyInstaller's dependency scanner would then pair Python's ``_ssl.pyd`` with
|
|
# those incompatible DLLs. Put the running interpreter's DLL directory first
|
|
# and collect the exact same files explicitly so the build is reproducible.
|
|
python_runtime_binaries = []
|
|
if sys.platform == "win32":
|
|
python_dll_dir = Path(sys.base_prefix) / "DLLs"
|
|
for dll_name in ("libssl-3-x64.dll", "libcrypto-3-x64.dll"):
|
|
dll_path = python_dll_dir / dll_name
|
|
if not dll_path.is_file():
|
|
raise SystemExit(f"Python runtime dependency is missing: {dll_path}")
|
|
python_runtime_binaries.append((str(dll_path), "."))
|
|
os.environ["PATH"] = os.pathsep.join((str(python_dll_dir), os.environ.get("PATH", "")))
|
|
|
|
qt_webengine_hiddenimports = [
|
|
# Importing these modules lets PyInstaller's official Qt hooks collect the
|
|
# helper executable/app, resources, locales, frameworks, and plugins.
|
|
"PySide6.QtWebChannel",
|
|
"PySide6.QtWebEngineCore",
|
|
"PySide6.QtWebEngineWidgets",
|
|
"PySide6.QtNetwork",
|
|
"PySide6.QtPrintSupport",
|
|
]
|
|
|
|
qt_multimedia_hiddenimports = [
|
|
# These are intentionally explicit instead of relying on imports hidden by
|
|
# the application's optional media fallback. PyInstaller's official
|
|
# hooks collect Qt6Multimedia*.dll/.dylib/framework, plugins/multimedia,
|
|
# and the platform FFmpeg/native backend dependencies.
|
|
"PySide6.QtMultimedia",
|
|
"PySide6.QtMultimediaWidgets",
|
|
]
|
|
|
|
analysis = Analysis(
|
|
[str(ENTRY_POINT)],
|
|
pathex=[str(SOURCE_ROOT)],
|
|
binaries=python_runtime_binaries,
|
|
datas=[
|
|
(str(VIDEO_DIST), "video_companion_dist"),
|
|
(str(RESOURCES), "resources"),
|
|
],
|
|
hiddenimports=qt_webengine_hiddenimports + qt_multimedia_hiddenimports,
|
|
hookspath=[],
|
|
hooksconfig={},
|
|
runtime_hooks=[str(MEDIA_SMOKE_HOOK)],
|
|
excludes=[],
|
|
noarchive=False,
|
|
optimize=0,
|
|
)
|
|
|
|
pyz = PYZ(analysis.pure)
|
|
|
|
is_macos = sys.platform == "darwin"
|
|
exe = EXE(
|
|
pyz,
|
|
analysis.scripts,
|
|
[],
|
|
exclude_binaries=True,
|
|
name="DoctorWorkstation",
|
|
debug=False,
|
|
bootloader_ignore_signals=False,
|
|
strip=False,
|
|
upx=False,
|
|
console=False,
|
|
disable_windowed_traceback=False,
|
|
argv_emulation=False,
|
|
target_arch=None,
|
|
icon=str(WINDOWS_ICON) if sys.platform == "win32" else None,
|
|
codesign_identity=os.environ.get("MACOS_CODESIGN_IDENTITY") if is_macos else None,
|
|
entitlements_file=str(ENTITLEMENTS) if is_macos else None,
|
|
version=str(VERSION_FILE) if VERSION_FILE else None,
|
|
)
|
|
|
|
collection = COLLECT(
|
|
exe,
|
|
analysis.binaries,
|
|
analysis.datas,
|
|
strip=False,
|
|
upx=False,
|
|
name="DoctorWorkstation",
|
|
)
|
|
|
|
if is_macos:
|
|
app = BUNDLE(
|
|
collection,
|
|
name="DoctorWorkstation.app",
|
|
icon=str(MACOS_ICON),
|
|
bundle_identifier="com.zyt.doctor-workstation",
|
|
info_plist={
|
|
"CFBundleDisplayName": "甄养堂医生工作站",
|
|
"NSCameraUsageDescription": "用于视频面诊时采集医生画面。",
|
|
"NSMicrophoneUsageDescription": "用于视频面诊时采集医生语音。",
|
|
"NSHighResolutionCapable": True,
|
|
},
|
|
)
|