--- description: "Use when building, compiling, packaging or releasing the Kiwy Signage Player: PyInstaller build, .exe generation, build.spec, hiddenimports, code signing, Smart App Control, Windows development, Raspberry Pi deployment, or adding new modules under src/. Covers the exact build commands, environment constraints, bundling rules and verification steps." name: "Kiwy Build & Development" --- # Kiwy Signage Player — Build & Development Cross-platform Kivy digital signage player. - **Raspberry Pi / Linux** — `src/main.py` is the entry point (root `install.sh`, `start.sh`). - **Windows** — `windows/run_win.py` is the entry point; it sets Windows env vars, imports `main.py`, then monkey-patches platform differences. Packaged to `.exe` with PyInstaller. ## Ground Rules - **Keep `src/main.py` cross-platform.** Windows-specific behaviour belongs in `windows/run_win.py` (see `_patch_main()`), Pi-specific behaviour in `main.py` guarded by capability checks. Do not add Windows-only imports to `main.py`. - **Rebuild is mandatory.** The `.exe` bundles `src/`, so Python edits are invisible until you rebuild. There is no hot reload in the packaged app. - **`dist/` is a runtime folder, not just build output.** It holds `config/`, `media/`, `playlists/`, `logs/`, `.kiosk-profile/` and `.player_heartbeat`. Never wipe `dist/` blindly; a `--clean` rebuild preserves it, but a manual delete destroys player state. ## Environment | Item | Value | |------|-------| | Interpreter | **Python 3.12.9** (64-bit) — project venv at `windows\venv` | | Kivy | 2.3.1 | | PyInstaller | 6.21.0 | | Linux target | Python 3.13 (`repo/python-wheels/`, `repo/system-packages/`) | - **Python 3.13+/3.14 is NOT supported for the Windows build** — Kivy 2.3.1 has no wheels for them. - `cefpython3` is **not installed** in the project venv (its wheels stop at Python 3.9). The embedded-CEF engine in `windows/cef_browser.py` is therefore dormant on this build — do not rely on it. - **Web links use embedded WebView2** (`windows/webview2_browser.py`), driven through `pythonnet` (installed). Two extra things ship with the exe: - `windows/webview2_sdk/` — ~860 KB of SDK DLLs (`Microsoft.Web.WebView2.Core.dll`, `WebView2Loader.dll`). **Tracked in git on purpose**: without them WebView2 silently downgrades to the Chrome/Edge subprocess engine. - `windows/webview2_runtime/MicrosoftEdgeWebview2Setup.exe` — ~1.7 MB bootstrapper, used to install the Runtime on a machine that lacks it. The ~203 MB offline standalone installer is **git-ignored**; fetch it with `windows/webview2_runtime/download_runtime_installers.ps1 -Offline` when you need to deploy to machines with no internet (it makes the exe ~305 MB). ## Build From `windows\`: ``` venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm ``` Or the full one-click path (creates venv, installs deps, builds, then signs): ``` build_win.bat ``` Output (folder mode, via `COLLECT`): ``` windows\dist\KiwySignagePlayer\KiwySignagePlayer.exe <- the real build output ``` **Deployment hazard:** `windows\dist\KiwySignagePlayer.exe` (one level up) is a stale leftover from an older single-file build of the same name. It is not refreshed by the current spec, so deploying it ships old code. Always take the executable from the `KiwySignagePlayer\` subfolder, and delete the stray copy if it reappears. ### `build.spec` facts - Entry point `run_win.py`; `pathex=[BUILD_DIR, SRC_DIR]` so `src/` modules resolve. - `runtime_hooks=[pyi_runtime_hook.py]` — sets per-monitor DPI awareness **before** SDL/Kivy initialise. Keep DPI work here; it must run before any window is created. - `datas += Tree(src)` bundles all of `src/`. `console=True` so startup errors and the Kivy log remain visible — do not flip this to `False` without a reason. - `hiddenimports` must list modules that PyInstaller's static analysis **cannot** see: lazily imported ones (`cef_browser` is imported inside a function) or dynamic imports (`getattr`, `importlib`). Top-level imports such as `weblink_session` are found automatically via `pathex`, but listing them is harmless insurance. **When you add a module under `src/` that is imported lazily or by string, add it to `hiddenimports` or the packaged exe will fail at runtime with `ModuleNotFoundError`.** - `excluded_imports` drops Linux-only packages (`evdev`, `gi`, GStreamer) and the non-matching `cefpython3` `.pyd` variants. ## First-run setup (no bundled credentials) `config/app_config.json` is **deliberately NOT bundled** into the exe, and `player_auth.json` is excluded from `Tree(src)`. Shipping either one planted the build machine's `server_ip` / `screen_name` / `auth_code` into every install, so a fresh machine silently connected to the wrong server (or played a stale playlist) instead of asking. What happens on a machine with no usable config: 1. The splash video (`config/resources/intro1.mp4`) plays. 2. A notice appears: *"Player is not configured"*. 3. After `SETUP_NOTICE_SECONDS` (**5 s**, `src/main.py`) the Settings screen opens automatically. 4. Saving valid values writes `config/app_config.json` next to the **.exe** and playback starts immediately — no restart needed. On a machine that **is** configured, the notice and Settings are skipped and the cached playlist plays straight away. Key functions in `src/main.py`: | Name | Role | |------|------| | `config_is_configured(config)` | Single source of truth. Missing file, empty/unparseable JSON, missing keys and leftover placeholders (`localhost`, `kivy-player`, `1234567`, `127.0.0.1`) all count as **unconfigured**. | | `DEFAULT_CONFIG` | Blank-credential starting point, so a fresh install can never look configured. | | `SignagePlayer.on_intro_finished()` | The one decision point after the splash: setup vs. playback. Both intro paths (video end and "no intro file") go through it. | | `show_setup_required_notice()` | Shows the notice, then opens Settings after 5 s. | | `on_first_run_config_saved()` | Re-syncs and starts playback. | `config_is_configured()` is covered by `windows/test_first_run_setup.py` — run it after touching that logic. > When adding a new **required** config key, add it to `CONFIG_REQUIRED_KEYS` > (and to `CONFIG_PLACEHOLDER_VALUES` if it has a placeholder default), or the > first-run flow will not ask for it. ## 24/7 Supervision (Windows) A signage player must survive crashes and hangs unattended. On **Linux/Pi** this is `start.sh` (systemd or `./start.sh`). On **Windows** it is `windows/watchdog.ps1`, launched by `windows/start_player_watchdog.bat`. It restarts the player when it: - **crashed** — the process disappeared; or - **hung** — the process is alive but `.player_heartbeat` has gone stale (the player rewrites it every 10 s; the watchdog treats >60 s as hung). A frozen player is otherwise indistinguishable from a working one. It also backs off if the player cannot stay up (crash-loop breaker: `CrashLoopMaxFailures` failures in `CrashLoopWindowMin` minutes without ever becoming healthy → `CrashLoopBackoffMin` minutes of quiet). ### The stop flag is SESSION-SCOPED The only supported way to stop the player is the **exit-screen password**. On success the player writes `.player_stop_requested` next to its `.exe`, and the watchdog then **stands down instead of restarting**. The flag is cleared **every time the watchdog starts**, which is what makes it session-scoped: the password exit ends the *current* session, and the next launch begins a new one. There is no file to delete by hand. > **Ordering matters:** the flag is the operationally-correct choice over a > "remember the last exit" setting, because a reboot must come back up playing. > Clearing on start also means a power cut cannot leave the player permanently off. ### Two failure modes that must not be confused | Situation | Correct reaction | |---|---| | Was healthy, then heartbeat went stale | **Restart promptly** (this is a hang) | | Running but never became healthy (slow start, bad install, the **first-run setup screen**) | **Be patient**, then restart after `2 × StartupGraceSec` | Never kill a player that has not yet been healthy — it may be an operator entering server settings on the first-run setup screen. ### Important Windows-specific facts - The packaged player is **TWO processes** (PyInstaller bootloader parent + the child that owns the SDL window). Any kill must use `taskkill /T` or the visible window survives and the next launch collides with it. - The watchdog can only *restart* an install that already has a working config. It cannot help before the player has first run. - Hiding the console window (`console=False`) is **not** done: the console is the only way to see a start-up failure, and the build instructions warn against flipping it without reason. - **No Windows service.** A service runs in session 0 with no desktop, so the player could not render to the screen. A login-triggered task is the correct Windows counterpart to the Pi's systemd unit — that is what `start_player_watchdog.bat` is for (add it to the Startup folder for autostart). ### Verify the watchdog ``` cd windows venv\Scripts\python.exe test_watchdog.py # crash + hang + stop-flag + new-session ``` Check state at any time (read-only): ``` powershell -NoProfile -ExecutionPolicy Bypass -File windows\watchdog.ps1 -Status ``` > When testing a hang, do **not** just backdate the heartbeat file's mtime — the > healthy player rewrites it immediately, so nothing is actually stale and the > test passes for the wrong reason. Hold the file open with an exclusive Windows > lock (share mode 0) so the player's own write fails; that reproduces a genuine > stale heartbeat while the process stays alive. ## Code Signing (production constraint) Production PCs run with **Smart App Control enforced** (`VerifiedAndReputablePolicyState=1`, UMCI enforced) with **no "Run anyway" bypass** — an unsigned exe is blocked at kernel level. - A **self-signed certificate does NOT satisfy Smart App Control.** A cert from a reputable public CA is required. `create_self_signed_cert.ps1` is dev-only. - Provide a cert as `KIWY_SIGN_PFX` (+ optional `KIWY_SIGN_PFX_PASSWORD`) or drop `kiwy_signing.pfx` in `windows\`; `build_win.bat` then signs via `sign_exe.ps1` (signtool with RFC3161 timestamp when available). - See `documentation/CODE_SIGNING_SMART_APP_CONTROL.md`. ## Verify Before Committing Fast syntax gate (no build, seconds): ``` python -m py_compile src/main.py src/weblink_session.py windows/run_win.py windows/webview2_browser.py windows/webview2_runtime.py ``` Web-link engine checks (no build, no player): ``` cd windows venv\Scripts\python.exe test_webview2_embed.py # embeds WebView2 in a bare Win32 window venv\Scripts\python.exe test_webview2_runtime.py # Runtime detection + installer discovery ``` Rebuild warning: the bundled offline installer makes the exe ~305 MB. If you do not need offline machines, delete `windows/webview2_runtime/MicrosoftEdgeWebView2RuntimeInstallerX64.exe` before building. **Close the running player before rebuilding.** A running `dist\KiwySignagePlayer\KiwySignagePlayer.exe` locks the output and the build fails with "Access is denied". Chrome/Edge kiosk processes left over from web-link playback can also hold locks; if the build fails, check for stray `msedge.exe` / `chrome.exe` before retrying. Because `.exe` is a **reserved Windows device name** as well as a real file, use `Test-Path -LiteralPath` when probing for the executable, otherwise the path silently resolves to the console device. Development run — **use this on a SAC-locked host**, since the unsigned exe cannot launch: ``` cd windows venv\Scripts\activate python run_win.py ``` Diagnose playback transitions via the trace log (`logs\playback_trace.log`), written by `src/playback_trace.py`. ## Release Checklist 1. Bump `PLAYER_VERSION` in `src/main.py` **and** `filevers`/`prodvers`/`FileVersion`/ `ProductVersion` in `windows\version_info.txt`; keep them in sync. 2. Rebuild (`build_win.bat`). 3. Confirm the exe is signed (`Get-AuthenticodeSignature`); unsigned builds are blocked on production hosts. 4. Smoke-test a mixed playlist: image → video → weblink → image, verifying durations, audio (`audio: off` / `muted`), and a clean exit/restart. ## Commit Hygiene - Never commit `windows\build\` or `windows\dist\` (git-ignored). Tracked build output such as `windows\archive_list.txt` and `windows\build_last.txt` is the exception, not the rule. - **Do not commit `player_auth.json` or `src/player_auth.json`** — they contain live credentials (`auth_code`, `player_id`, `server_url`). These are currently tracked; treat any future change to them as a deliberate, reviewed decision. - Do not commit `config\app_config.json` values that are host-specific without checking whether they belong in the repo default.