"""Build script: compile PaperDash server to a standalone .exe with system tray. Usage: python build_exe.py # Build in release mode python build_exe.py --debug # Build in debug mode (with console) Output: server/dist/PaperDash.exe """ from __future__ import annotations import argparse import os import subprocess import sys SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) DIST_DIR = os.path.join(SCRIPT_DIR, "dist") def build(debug: bool = False) -> None: """Compile the server to an .exe using PyInstaller.""" os.makedirs(DIST_DIR, exist_ok=True) # PyInstaller spec content — wraps the server with system tray spec_content = f''' # -*- mode: python ; coding: utf-8 -*- """PyInstaller spec for PaperDash with system tray.""" import os from pathlib import Path block_cipher = None a = Analysis( ["tray_wrapper.py"], pathex=[], binaries=[], datas=[ ("settings.html", "."), ("settings.css", "."), ], hiddenimports=[ "fastapi", "uvicorn", "pydantic", "pystray", "PIL", "audio_mixer", "media", "notifications", "telemetry", "main", "yaml", ], hookspath=[], hooksconfig={{}}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, a.binaries, a.datas, [], name="PaperDash", debug={"True": True, "False": False}[str(debug)], bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False if not debug else True, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon=None, onefile=True, ) ''' spec_path = os.path.join(SCRIPT_DIR, "PaperDash.spec") with open(spec_path, "w") as f: f.write(spec_content) try: cmd = [ sys.executable, "-m", "PyInstaller", spec_path, "--clean", ] if debug: cmd.append("--debug=all") print(f"Building PaperDash.exe (debug={debug})...") print(f" Spec: {spec_path}") print(f" Output: {DIST_DIR}/PaperDash.exe") print() subprocess.run(cmd, check=True) exe_path = os.path.join(DIST_DIR, "PaperDash.exe") if os.path.exists(exe_path): size_mb = os.path.getsize(exe_path) / (1024 * 1024) print(f"\nBuild complete: {exe_path} ({size_mb:.1f} MB)") else: print("\nBuild completed but .exe not found in dist/") except subprocess.CalledProcessError as e: print(f"\nBuild failed: {e}", file=sys.stderr) sys.exit(1) finally: # Clean up spec file if os.path.exists(spec_path): os.remove(spec_path) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Build PaperDash .exe") parser.add_argument("--debug", action="store_true", help="Build with debug console") args = parser.parse_args() build(debug=args.debug)