# -*- mode: python ; coding: utf-8 -*- # # clawdmeter.spec — build the standalone Windows daemon + tray executable. # # Produces a single, self-contained dist\Clawdmeter.exe that bundles its own # Python, bleak (WinRT BLE) and the winrt media projection — so it runs on ANY # Windows 11 machine with no Python and no pip install. The exe is the tray app # (daemon thread + notification-area icon + login-autostart toggle); it is built # windowed, so there is no console window and it logs to # %LOCALAPPDATA%\Clawdmeter\daemon.log. # # Build (from the repo root, inside the venv) — or just run build-exe.ps1: # .venv\Scripts\python.exe -m PyInstaller --noconfirm clawdmeter.spec # # winrt + bleak ship C-extension projections (.pyd) that PyInstaller's static # analysis misses: bleak imports its WinRT backend dynamically, and the # winrt.windows.* namespaces are split across separate distributions. collect_all # pulls in their submodules, binaries and metadata so both BLE (bluetooth) and the # now-playing media session work inside the frozen exe. Verified on hardware: # connects, reads the media session, and pushes Cyrillic now-playing payloads. from PyInstaller.utils.hooks import collect_all datas = [ ('firmware/src/logo.h', 'firmware/src'), # tray icon parsed at runtime ('daemon/web', 'daemon/web'), # Phase 7 control-panel UI (server._web_dir) ] binaries = [] hiddenimports = [ # Imported lazily inside tray_windows.main(), so name them explicitly. 'daemon.claude_usage_daemon_windows', 'daemon.autostart_windows', 'daemon.icon_assets', # Phase 7 control panel (lazy string-imports inside tray_windows / server). 'daemon.config', 'daemon.server', 'daemon.panel', 'daemon.ha_client', # v3 provider package (registry + providers are lazy-imported in the loop). 'daemon.providers', 'daemon.providers.base', 'daemon.providers.anthropic', 'daemon.providers.openai_codex', # The exact winrt media modules read_now_playing() pulls in. 'winrt.windows.media', 'winrt.windows.media.control', # pywebview's Windows backend + pythonnet bridge load these dynamically. 'webview.platforms.edgechromium', 'clr', ] # Phase 7 adds the FastAPI control panel + the pywebview WebView2 window. uvicorn # and pywebview both import their submodules (loop/protocol pickers; the # edgechromium backend + bundled WebView2 DLLs) dynamically, which PyInstaller's # static analysis misses — collect_all pulls submodules, binaries and data files. # clr_loader/pythonnet ship the .NET runtime-config JSON pywebview's WinForms host # needs. If a package is absent the build fails loudly (better than a silent # blank window at runtime). for _pkg in ('winrt', 'bleak', 'pystray', 'PIL', 'fastapi', 'uvicorn', 'webview', 'clr_loader', 'pythonnet'): _d, _b, _h = collect_all(_pkg) datas += _d binaries += _b hiddenimports += _h a = Analysis( ['daemon\\tray_windows.py'], pathex=['.'], binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=['tkinter', 'pytest'], noarchive=False, optimize=0, ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, a.binaries, a.datas, [], name='Clawdmeter', debug=False, bootloader_ignore_signals=False, strip=False, upx=False, # UPX compression raises AV false-positive rates — leave it off upx_exclude=[], runtime_tmpdir=None, console=False, # windowed tray app: no console window; logs to daemon.log disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, )