v2: standalone Clawdmeter.exe — runs on any Win11 machine, no Python

Package the tray+daemon as a single PyInstaller onefile exe so it runs on a
fresh Windows 11 box with no Python and no pip install. Verified on hardware:
the frozen exe connects over BLE, reads the Windows media session, and pushes
now-playing (incl. Cyrillic) at the 3s / 60s cadence.

- clawdmeter.spec: onefile, windowed (no console; logs to daemon.log). collect_all
  for winrt + bleak is the key bit — bleak loads its WinRT backend dynamically and
  the winrt.windows.* projections are split distributions that PyInstaller's static
  analysis misses, so BLE and the media session would otherwise fail at runtime.
  upx off (lowers AV false-positive rate).
- build-exe.ps1: one-command build (venv + deps + pyinstaller + spec).
- tray_windows.py: resolve the brand-logo asset from sys._MEIPASS when frozen.
- autostart_windows.py: when frozen, the HKCU\Run "Start at login" value points at
  the exe itself (sys.executable), not pythonw + script.
- .gitignore: ignore /build, /dist, /.venv — the exe ships via a Gitea release.
- README-windows.md: document the standalone-exe path (download / run / build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wenil
2026-06-21 09:58:26 +03:00
co-authored by Claude Opus 4.8
parent 650af4221b
commit ec8322fa21
6 changed files with 186 additions and 3 deletions
+41 -2
View File
@@ -59,7 +59,46 @@ pairing disables the keyboard buttons.
---
## Setup (one time)
## Standalone executable — no Python required (recommended)
To run Clawdmeter on a machine **without Python**, use the single-file
`Clawdmeter.exe`. It bundles its own Python, the WinRT BLE stack and the
media-session reader, so nothing needs to be installed.
1. Pair the device with Windows once (see [Pair the device](#pair-the-device-one-time)).
2. Get `Clawdmeter.exe` — download it from the project's Gitea release, or build it
yourself (below).
3. Double-click `Clawdmeter.exe`. The tray icon appears and the watch starts
updating within ~10 seconds.
4. To launch it automatically at every logon, right-click the tray icon →
**Start at login**. That registers the exe itself under
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run` — no Python, no console window.
There is no console window; the exe logs to `%LOCALAPPDATA%\Clawdmeter\daemon.log`.
### Building the exe
On a machine that *does* have Python (e.g. your dev box), from the repo root:
```powershell
powershell -ExecutionPolicy Bypass -File build-exe.ps1
```
This creates `dist\Clawdmeter.exe` (~3060 MB). The PyInstaller config lives in
`clawdmeter.spec`. The exe is intentionally **not** committed to git — distribute
it through a Gitea release.
> **SmartScreen / antivirus:** unsigned PyInstaller executables are sometimes
> flagged by a generic heuristic (not a real detection). Until the exe is
> code-signed, you may need to allow it through SmartScreen ("More info → Run
> anyway"). Building from source sidesteps this entirely.
---
## Setup from source (one time)
> Use this if you prefer running from a Python checkout instead of the standalone
> exe above (e.g. on your dev box).
Open a PowerShell terminal and `cd` to the repository root.
@@ -224,5 +263,5 @@ launched.
## What is NOT covered here
- PyInstaller / one-file `.exe` packaging — v2
- Code-signing the standalone `.exe` (to avoid SmartScreen/AV prompts) — future
- MAC-address cache / sleep-wake reconnect hardening — Phase 3
+5
View File
@@ -56,6 +56,11 @@ def _command(tray_script: str | None = None) -> str:
module's own path (useful when autostart_windows.py IS
the entry point, but callers should pass tray_windows.py).
"""
if getattr(sys, "frozen", False):
# Running as the bundled PyInstaller exe: autostart launches the exe
# itself (sys.executable), not a pythonw + script pair. The exe is built
# windowed, so this is genuinely consoleless — no script path involved.
return f'"{sys.executable}"'
pythonw = os.path.join(sys.base_exec_prefix, "pythonw.exe")
script = os.path.abspath(tray_script if tray_script is not None else __file__)
return f'"{pythonw}" "{script}"'
+7 -1
View File
@@ -27,7 +27,13 @@ import time
# imports below and the brand-logo asset load work no matter what the current
# working directory is — critical for logon autostart, where the HKCU\Run entry
# starts with cwd = System32, not the repo (APP-01 / SC#1).
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if getattr(sys, "frozen", False):
# PyInstaller bundle: the `daemon` package loads from the frozen archive and
# data files (the brand logo.h) live under sys._MEIPASS. Point _REPO_ROOT at
# the bundle root so the logo asset path below resolves inside the exe.
_REPO_ROOT = sys._MEIPASS # type: ignore[attr-defined]
else:
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)