# -*- 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 _find_share_dlls(package_path, subdir='bin'): """Find .dll files under a package's share/ directory.""" if not package_path: return [] base = _Path(package_path).parent # Check: share//bin/ relative to parent share = base / 'share' if share.is_dir(): 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 return [] # SDL2 DLLs _sdl2_spec = importlib.util.find_spec('kivy_deps.sdl2') _sdl2_dlls = _find_share_dlls(_sdl2_spec.origin if _sdl2_spec else None) # ANGLE DLLs _angle_spec = importlib.util.find_spec('kivy_deps.angle') _angle_dlls = _find_share_dlls(_angle_spec.origin if _angle_spec else None) # GLEW DLLs _glew_spec = importlib.util.find_spec('kivy_deps.glew') _glew_dlls = _find_share_dlls(_glew_spec.origin if _glew_spec else None) # ffpyplayer FFmpeg DLLs _ffpy_spec = importlib.util.find_spec('ffpyplayer') _ffpy_dlls = _find_share_dlls(_ffpy_spec.origin if _ffpy_spec else None) _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) # --- 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(RESOURCES_DIR / 'app_icon.ico') if (RESOURCES_DIR / 'app_icon.ico').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', )