"""Build Ameba Control Panel executable with PyInstaller.""" from __future__ import annotations import argparse import os import sys from pathlib import Path def _add_data(src: Path, dest: str) -> str: sep = ";" if os.name == "nt" else ":" return f"{src}{sep}{dest}" def _write_version_file(root: Path, version: str) -> Path: parts = (version.split(".") + ["0", "0", "0"])[:4] csv = ", ".join(parts) dot = ".".join(parts) out = root / "build" / "version_info.txt" out.parent.mkdir(parents=True, exist_ok=True) out.write_text(f"""\ VSVersionInfo( ffi=FixedFileInfo(filevers=({csv}), prodvers=({csv})), kids=[ StringFileInfo([StringTable('040904B0', [ StringStruct('CompanyName', 'Realtek'), StringStruct('FileDescription', 'Ameba Control Panel'), StringStruct('FileVersion', '{dot}'), StringStruct('ProductName', 'Ameba Control Panel'), StringStruct('ProductVersion', '{dot}'), ])]), VarFileInfo([VarStruct('Translation', [0x0409, 0x04B0])]) ] ) """, encoding="utf-8") return out _EXCLUDES = [ "tkinter", "matplotlib", "numpy", "scipy", "pandas", "PIL", "PySide6.QtWebEngine", "PySide6.QtWebEngineCore", "PySide6.QtWebEngineWidgets", "PySide6.Qt3DCore", "PySide6.Qt3DRender", "PySide6.Qt3DInput", "PySide6.Qt3DAnimation", "PySide6.Qt3DExtras", "PySide6.Qt3DLogic", "PySide6.QtCharts", "PySide6.QtDataVisualization", "PySide6.QtMultimedia", "PySide6.QtMultimediaWidgets", "PySide6.QtQuick", "PySide6.QtQuick3D", "PySide6.QtQuickWidgets", "PySide6.QtQml", "PySide6.QtRemoteObjects", "PySide6.QtSensors", "PySide6.QtSerialBus", "PySide6.QtBluetooth", "PySide6.QtNfc", "PySide6.QtPositioning", "PySide6.QtLocation", "PySide6.QtTest", "PySide6.QtPdf", "PySide6.QtPdfWidgets", "PySide6.QtSvgWidgets", "PySide6.QtNetworkAuth", "PySide6.QtDesigner", "PySide6.QtHelp", "PySide6.QtOpenGL", "PySide6.QtOpenGLWidgets", "PySide6.QtSpatialAudio", "PySide6.QtStateMachine", "PySide6.QtTextToSpeech", "PySide6.QtHttpServer", "PySide6.QtGraphs", ] def build(*, onefile: bool, icon: str | None = None, splash: str | None = None) -> None: root = Path(__file__).resolve().parent.parent entry = root / "script" / "auto_run.py" flash_dir = root / "Flash" if not entry.exists(): sys.exit(f"Entry script missing: {entry}") if not flash_dir.exists(): sys.exit(f"Flash folder missing: {flash_dir}") os.chdir(root) try: import PyInstaller.__main__ as pyinstaller except ImportError: sys.exit("PyInstaller is not installed. Run: pip install PyInstaller") sys.path.insert(0, str(root)) from ameba_control_panel.config import APP_VERSION version_file = _write_version_file(root, APP_VERSION) args = [ "--noconfirm", "--clean", "--windowed", "--onefile" if onefile else "--onedir", f"--name=AmebaControlPanel", f"--distpath={root / 'dist'}", f"--workpath={root / 'build'}", f"--specpath={root}", f"--version-file={version_file}", "--paths", str(root), "--collect-all", "PySide6", "--hidden-import=serial", "--hidden-import=serial.tools.list_ports", "--hidden-import=pyDes", "--hidden-import=colorama", "--add-data", _add_data(flash_dir, "Flash"), ] for mod in _EXCLUDES: args.extend(["--exclude-module", mod]) if icon: icon_path = Path(icon) if icon_path.exists(): args.extend(["--icon", str(icon_path)]) else: print(f"Warning: icon not found: {icon_path}", file=sys.stderr) if splash: splash_path = Path(splash) if splash_path.exists(): args.extend(["--splash", str(splash_path)]) else: print(f"Warning: splash image not found: {splash_path}", file=sys.stderr) args.append(str(entry)) pyinstaller.run(args) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Build Ameba Control Panel EXE") parser.add_argument("--onefile", action="store_true", help="Create single-file exe (slower startup)") parser.add_argument("--icon", help="Path to .ico file") parser.add_argument("--splash", help="Path to splash screen image (.png)") opts = parser.parse_args() # Default to --onedir for fast startup build(onefile=opts.onefile, icon=opts.icon, splash=opts.splash)