Files

90 lines
3.0 KiB
Python

"""Build the Tk controller with Windows Python and PyInstaller.
Run `uv run --python 3.14 --group bundle python scripts/build_exe.py`.
Build output stays under dist/ and temporary files under build/. A Windows
executable must be built on Windows; this script never attempts cross-compiling.
"""
import argparse
import importlib.util
import subprocess
import sys
from pathlib import Path
def build_arguments(root, onefile=False, console=False):
"""Include the dynamically selected WinRT backend and its namespace modules."""
root = Path(root).resolve()
return [
"--noconfirm",
"--clean",
"--name",
"SoundTowerController",
"--console" if console else "--windowed",
"--onefile" if onefile else "--onedir",
"--distpath",
str(root / "dist"),
"--workpath",
str(root / "build" / "pyinstaller"),
"--specpath",
str(root / "build"),
"--paths",
str(root),
# Bleak chooses backends dynamically. WinRT's generated namespaces
# and extension modules must also be present in the frozen application.
"--collect-submodules",
"bleak.backends.winrt",
"--collect-all",
"winrt",
"--copy-metadata",
"bleak",
"--copy-metadata",
"winrt-runtime",
str(root / "controller" / "__main__.py"),
]
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--onefile",
action="store_true",
help="Make one self-extracting .exe instead of an application folder",
)
parser.add_argument(
"--console", action="store_true", help="Keep a console for diagnosing startup errors"
)
args = parser.parse_args(argv)
if sys.platform != "win32":
parser.error(
"Build with Windows Python in PowerShell, or Windows uv.exe from WSL. Linux Python cannot build a Windows executable."
)
if importlib.util.find_spec("PyInstaller") is None:
parser.error(
"PyInstaller is missing. Run with: uv run --group bundle python scripts/build_exe.py"
)
if importlib.util.find_spec("tkinter") is None:
parser.error(
"This Python installation lacks Tk. Use --python with a Windows Python installation that includes Tcl/Tk."
)
root = Path(__file__).resolve().parents[1]
subprocess.run(
[sys.executable, "-m", "PyInstaller", *build_arguments(root, args.onefile, args.console)],
cwd=root,
check=True,
)
executable = (
root / "dist" / "SoundTowerController.exe"
if args.onefile
else root / "dist" / "SoundTowerController" / "SoundTowerController.exe"
)
if not executable.is_file():
raise RuntimeError(f"Build finished without the expected executable: {executable}")
print(f"Built: {executable}")
if not args.onefile:
print("Keep the whole SoundTowerController folder together, including _internal.")
if __name__ == "__main__":
main()