# -*- mode: python ; coding: utf-8 -*- """ PyInstaller spec file for Kiwy Signage Player (Windows .exe) Build command (from windows/ directory): pyinstaller build.spec --clean --noconfirm OR use the build script: build_win.bat """ import os import sys from pathlib import Path # --- Paths ----------------------------------------------------------- # This spec file is in windows/build.spec, so the project root is # always two levels up from this file's real location. # __file__ may not be available in PyInstaller spec context fallback to cwd. try: _spec_dir = Path(__file__).resolve().parent except NameError: _spec_dir = Path(os.getcwd()).resolve() # _spec_dir is now the absolute path to the windows/ directory BUILD_DIR = _spec_dir ROOT_DIR = BUILD_DIR.parent SRC_DIR = ROOT_DIR / 'src' CONFIG_DIR = ROOT_DIR / 'config' RESOURCES_DIR = CONFIG_DIR / 'resources' # --- Determine hidden imports that PyInstaller might miss ------------- hidden_imports = [ # Kivy core modules 'kivy.core.window', 'kivy.core.video', 'kivy.core.audio', 'kivy.core.text', 'kivy.core.image', 'kivy.core.gl', 'kivy.core.camera', 'kivy.core.clipboard', 'kivy.core.spelling', 'kivy.core.text.markup', 'kivy.core.window.window_sdl2', 'kivy.core.image.img_sdl2', 'kivy.core.video.video_ffpyplayer', 'kivy.core.audio.audio_ffpyplayer', # Kivy modules 'kivy.uix.video', 'kivy.uix.vkeyboard', 'kivy.uix.popup', 'kivy.uix.image', 'kivy.uix.button', 'kivy.uix.label', 'kivy.uix.textinput', 'kivy.uix.boxlayout', 'kivy.uix.floatlayout', 'kivy.uix.slider', 'kivy.uix.widget', 'kivy.uix.checkbox', 'kivy.graphics', 'kivy.graphics.texture', 'kivy.graphics.vertex_instructions', 'kivy.graphics.context_instructions', 'kivy.clock', 'kivy.loader', 'kivy.animation', 'kivy.lang', 'kivy.logger', 'kivy.config', 'kivy.properties', 'kivy.metrics', 'kivy.factory', # Graphics providers 'kivy.graphics.opengl', 'kivy.graphics.opengl_utils', 'kivy.graphics.fbo', 'kivy.graphics.gl_instructions', 'kivy.graphics.stencil_instructions', 'kivy.graphics.scissor_instructions', 'kivy.graphics.buffer', 'kivy.graphics.vbo', 'kivy.graphics.shader', 'kivy.graphics.compiler', # ffpyplayer 'ffpyplayer', 'ffpyplayer.player', 'ffpyplayer.pic', 'ffpyplayer.writer', # Networking 'requests', 'aiohttp', 'urllib3', 'certifi', 'bcrypt', # Platform 'ctypes', 'ctypes.wintypes', 'subprocess', 'shutil', 'glob', 'selectors', 'tempfile', # Windows-specific 'cef_browser', 'win32gui', 'win32con', ] # Exclude Linux-only modules excluded_imports = [ 'gi', # GTK introspection (Linux) 'gi.repository', 'evdev', # We inject a fake evdev module in run_win.py # GStreamer — we use ffpyplayer, not GStreamer 'kivy.lib.gstplayer', # cefpython3: keep only Python 3.12 .pyd, exclude other version .pyd files 'cefpython3.cefpython_py27', 'cefpython3.cefpython_py34', 'cefpython3.cefpython_py35', 'cefpython3.cefpython_py36', 'cefpython3.cefpython_py37', 'cefpython3.cefpython_py38', 'cefpython3.cefpython_py39', 'cefpython3.cefpython_py310', 'cefpython3.cefpython_py311', ] # --- Application data files to bundle -------------------------------- # Resources (icons, intro video, etc.) resources_data = [] for item in RESOURCES_DIR.iterdir(): if item.is_file(): target_dir = 'config/resources' resources_data.append((str(item), target_dir)) # Config directory (app_config.json) config_data = [] config_file = CONFIG_DIR / 'app_config.json' if config_file.exists(): config_data.append((str(config_file), 'config')) # Source files - .kv file kv_file = SRC_DIR / 'signage_player.kv' kv_data = [] if kv_file.exists(): kv_data.append((str(kv_file), '.')) # Bundle the entire src directory as a tree source_tree = Tree(str(SRC_DIR), prefix='', excludes=['*.pyc', '__pycache__', '*.ini']) # --- Collect binary DLLs from kivy_deps and ffpyplayer ---------------- import importlib.util from pathlib import Path as _Path def _site_packages_dir(package_path): """Climb up from a package's __init__.py to its site-packages dir.""" d = _Path(package_path).parent while d.name != 'site-packages' and d.parent != d: d = d.parent return d def _find_share_dlls(package_name, share_name=None): """Find .dll files under venv_root/share//. kivy_deps.sdl2/angle/glew and ffpyplayer install their DLLs into /share//... NOT inside the package dir. The share folder is named after the *short* dep name (e.g. 'sdl2', 'angle', 'glew'), not the dotted package name ('kivy_deps.sdl2'), so pass share_name explicitly. """ if share_name is None: share_name = package_name spec = importlib.util.find_spec(package_name) if spec is None or not spec.origin: return [] sp = _site_packages_dir(spec.origin) # Climb from site-packages up until we find a sibling 'share' dir # (site-packages -> Lib -> venv, where venv/share lives). d = sp while d.parent != d: if (d.parent / 'share').is_dir(): share = d.parent / 'share' / share_name break d = d.parent else: return [] if not share.is_dir(): return [] results = [] for root, dirs, files in os.walk(share): for f in files: if f.endswith('.dll'): results.append((os.path.join(root, f), '.')) return results def _find_ffpyplayer_bins(): """Return ffpyplayer's own dependency DLL dirs (FFmpeg + bundled SDL). ffpyplayer ships a `dep_bins` list that already points at the correct share/ffpyplayer/ffmpeg/bin and share/ffpyplayer/sdl/bin directories. """ try: import ffpyplayer bins = getattr(ffpyplayer, 'dep_bins', None) if not bins: return [] results = [] for b in bins: bpath = _Path(b) if bpath.is_dir(): for f in bpath.glob('*.dll'): results.append((str(f), '.')) return results except Exception: return [] # SDL2 / ANGLE / GLEW DLLs (kivy_deps share dirs) _sdl2_dlls = _find_share_dlls('kivy_deps.sdl2', 'sdl2') _angle_dlls = _find_share_dlls('kivy_deps.angle', 'angle') _glew_dlls = _find_share_dlls('kivy_deps.glew', 'glew') # ffpyplayer FFmpeg + bundled SDL DLLs (via dep_bins) _ffpy_dlls = _find_ffpyplayer_bins() _all_binaries = _sdl2_dlls + _angle_dlls + _glew_dlls + _ffpy_dlls if not _all_binaries: print("=" * 70) print("WARNING: No Kivy/ffpyplayer DLLs found via share/ directories.") print("PyInstaller may still auto-detect them, but if the .exe") print("fails with 'SDL2.dll not found' or similar, you will need") print("to manually add the DLL paths to the spec file.") print("=" * 70) else: print(f"[spec] Bundling {len(_all_binaries)} DLLs:") for _p, _t in sorted(_all_binaries): print(f" {_Path(_p).name} <- {_p}") # --- Build the .exe -------------------------------------------------- a = Analysis( ['run_win.py'], # Entry point (relative to this spec) pathex=[str(BUILD_DIR), str(SRC_DIR)], # Where to find modules binaries=_all_binaries, datas=resources_data + config_data + kv_data, hiddenimports=hidden_imports, hookspath=[], hooksconfig={}, runtime_hooks=[str(BUILD_DIR / 'pyi_runtime_hook.py')], excludes=excluded_imports, noarchive=False, module_collection_mode={ 'kivy': 'pyz', 'kivy.core': 'pyz', 'kivy.uix': 'pyz', 'kivy.graphics': 'pyz', }, ) # Add the source tree (main.py, etc.) a.datas += source_tree pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='KiwySignagePlayer', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=True, # Show console for debugging startup errors disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon=str(BUILD_DIR / 'app_icon.ico') if (BUILD_DIR / 'app_icon.ico').exists() else None, version=str(BUILD_DIR / 'version_info.txt') if (BUILD_DIR / 'version_info.txt').exists() else None, ) # --- COLLECT everything into a single folder ------------------------- coll = COLLECT( exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, upx_exclude=[], name='KiwySignagePlayer', )