"""Build Ameba Control Panel Linux binary with PyInstaller (onedir).""" from __future__ import annotations import argparse import os import shutil import sys from pathlib import Path def _add_data(src: Path, dest: str) -> str: return f"{src}:{dest}" _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", ] _DESKTOP_ENTRY = """\ [Desktop Entry] Name=Ameba Control Panel Comment=Realtek Ameba device control panel for serial communication, flashing, and debugging Exec={exec_path} Icon={icon_path} Terminal=false Type=Application Categories=Development;Electronics; StartupWMClass=AmebaControlPanel """ _UDEV_RULES = """\ # Ameba Control Panel - USB serial device permissions # Install: sudo cp 99-ameba-serial.rules /etc/udev/rules.d/ # Reload: sudo udevadm control --reload-rules && sudo udevadm trigger # FTDI devices (FT232, FT2232, etc.) SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", MODE="0666" # Silicon Labs CP210x SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", MODE="0666" # WCH CH340/CH341 SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", MODE="0666" # Prolific PL2303 SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", MODE="0666" # Realtek devices SUBSYSTEM=="tty", ATTRS{idVendor}=="0bda", MODE="0666" # SEGGER J-Link SUBSYSTEM=="tty", ATTRS{idVendor}=="1366", MODE="0666" # STMicroelectronics STLink SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", MODE="0666" # Generic USB CDC ACM devices (covers most USB-to-serial adapters) SUBSYSTEM=="tty", KERNEL=="ttyACM[0-9]*", MODE="0666" SUBSYSTEM=="tty", KERNEL=="ttyUSB[0-9]*", MODE="0666" """ _LAUNCH_SCRIPT = """\ #!/bin/bash # Ameba Control Panel launcher # Handles X11/Wayland compatibility SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Detect display server and set Qt platform if needed if [ -n "$WAYLAND_DISPLAY" ]; then # Wayland session — let Qt use wayland or xcb fallback export QT_QPA_PLATFORM="${{QT_QPA_PLATFORM:-wayland;xcb}}" elif [ -n "$DISPLAY" ]; then # X11 session export QT_QPA_PLATFORM="${{QT_QPA_PLATFORM:-xcb}}" fi # Ensure bundled libs are found export LD_LIBRARY_PATH="$SCRIPT_DIR:$LD_LIBRARY_PATH" exec "$SCRIPT_DIR/AmebaControlPanel" "$@" """ def _generate_support_files(dist_dir: Path) -> None: """Generate udev rules, desktop entry, and launcher script.""" app_dir = dist_dir / "AmebaControlPanel" app_dir.mkdir(parents=True, exist_ok=True) # Udev rules rules_path = app_dir / "99-ameba-serial.rules" rules_path.write_text(_UDEV_RULES, encoding="utf-8") print(f" udev rules: {rules_path}") # Launcher script launcher = app_dir / "ameba-control-panel.sh" launcher.write_text(_LAUNCH_SCRIPT, encoding="utf-8") launcher.chmod(0o755) print(f" launcher: {launcher}") # Desktop entry desktop = app_dir / "ameba-control-panel.desktop" desktop.write_text( _DESKTOP_ENTRY.format( exec_path=str(app_dir / "ameba-control-panel.sh"), icon_path=str(app_dir / "icon.png") if (app_dir / "icon.png").exists() else "", ), encoding="utf-8", ) print(f" desktop: {desktop}") # Install helper install_script = app_dir / "install.sh" install_script.write_text( "#!/bin/bash\n" "set -e\n" 'SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"\n' "\n" "echo 'Installing udev rules for serial port access...'\n" 'sudo cp "$SCRIPT_DIR/99-ameba-serial.rules" /etc/udev/rules.d/\n' "sudo udevadm control --reload-rules\n" "sudo udevadm trigger\n" "echo 'Udev rules installed.'\n" "\n" "echo 'Installing desktop entry...'\n" 'DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"\n' 'mkdir -p "$DESKTOP_DIR"\n' 'sed "s|Exec=.*|Exec=$SCRIPT_DIR/ameba-control-panel.sh|" ' '"$SCRIPT_DIR/ameba-control-panel.desktop" > "$DESKTOP_DIR/ameba-control-panel.desktop"\n' "echo 'Desktop entry installed.'\n" "\n" "echo 'Done. You may need to log out and back in for serial port permissions to take effect.'\n", encoding="utf-8", ) install_script.chmod(0o755) print(f" installer: {install_script}") def build(*, icon: 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") args = [ "--noconfirm", "--clean", "--onedir", "--name=AmebaControlPanel", f"--distpath={root / 'dist'}", f"--workpath={root / 'build'}", f"--specpath={root}", "--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) args.append(str(entry)) print("Building with PyInstaller...") pyinstaller.run(args) # Copy icon if provided dist_app = root / "dist" / "AmebaControlPanel" if icon and Path(icon).exists() and dist_app.exists(): shutil.copy2(icon, dist_app / "icon.png") print("\nGenerating support files...") _generate_support_files(root / "dist") print(f"\nBuild complete: {dist_app}") print("\nTo install on target system:") print(f" 1. Copy {dist_app} to the target machine") print(f" 2. Run: {dist_app}/install.sh") print(f" 3. Launch: {dist_app}/ameba-control-panel.sh") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Build Ameba Control Panel for Linux") parser.add_argument("--icon", help="Path to .png icon file") opts = parser.parse_args() build(icon=opts.icon)