|
| 1 | +# -*- mode: python ; coding: utf-8 -*- |
| 2 | +""" |
| 3 | +PyInstaller spec file for LogiControl |
| 4 | +Produces a single-directory portable build in dist/LogiControl/ |
| 5 | +Run: pyinstaller LogiControl.spec |
| 6 | +""" |
| 7 | + |
| 8 | +import os |
| 9 | +import sys |
| 10 | +import shutil |
| 11 | +import PySide6 |
| 12 | + |
| 13 | +block_cipher = None |
| 14 | +ROOT = os.path.abspath(".") |
| 15 | +PYSIDE6_DIR = os.path.dirname(PySide6.__file__) |
| 16 | + |
| 17 | +a = Analysis( |
| 18 | + ["main_qml.py"], |
| 19 | + pathex=[ROOT], |
| 20 | + binaries=[], |
| 21 | + datas=[ |
| 22 | + # QML UI files |
| 23 | + (os.path.join(ROOT, "ui", "qml"), os.path.join("ui", "qml")), |
| 24 | + # Image assets |
| 25 | + (os.path.join(ROOT, "images"), "images"), |
| 26 | + ], |
| 27 | + hiddenimports=[ |
| 28 | + # conditional / lazy imports PyInstaller may miss |
| 29 | + "hid", |
| 30 | + "ctypes.wintypes", |
| 31 | + # PySide6 QML runtime |
| 32 | + "PySide6.QtQuick", |
| 33 | + "PySide6.QtQuickControls2", |
| 34 | + "PySide6.QtQml", |
| 35 | + "PySide6.QtNetwork", |
| 36 | + "PySide6.QtOpenGL", |
| 37 | + ], |
| 38 | + hookspath=[], |
| 39 | + hooksconfig={}, |
| 40 | + runtime_hooks=[], |
| 41 | + excludes=[ |
| 42 | + # ── Aggressively trim unneeded PySide6 modules ── |
| 43 | + "PySide6.QtWebEngine", |
| 44 | + "PySide6.QtWebEngineCore", |
| 45 | + "PySide6.QtWebEngineWidgets", |
| 46 | + "PySide6.QtWebChannel", |
| 47 | + "PySide6.QtWebSockets", |
| 48 | + "PySide6.Qt3DCore", |
| 49 | + "PySide6.Qt3DRender", |
| 50 | + "PySide6.Qt3DInput", |
| 51 | + "PySide6.Qt3DLogic", |
| 52 | + "PySide6.Qt3DAnimation", |
| 53 | + "PySide6.Qt3DExtras", |
| 54 | + "PySide6.QtMultimedia", |
| 55 | + "PySide6.QtMultimediaWidgets", |
| 56 | + "PySide6.QtBluetooth", |
| 57 | + "PySide6.QtNfc", |
| 58 | + "PySide6.QtPositioning", |
| 59 | + "PySide6.QtLocation", |
| 60 | + "PySide6.QtSensors", |
| 61 | + "PySide6.QtSerialPort", |
| 62 | + "PySide6.QtSerialBus", |
| 63 | + "PySide6.QtTest", |
| 64 | + "PySide6.QtPdf", |
| 65 | + "PySide6.QtPdfWidgets", |
| 66 | + "PySide6.QtCharts", |
| 67 | + "PySide6.QtDataVisualization", |
| 68 | + "PySide6.QtRemoteObjects", |
| 69 | + "PySide6.QtScxml", |
| 70 | + "PySide6.QtSql", |
| 71 | + "PySide6.QtSvg", |
| 72 | + "PySide6.QtSvgWidgets", |
| 73 | + "PySide6.QtTextToSpeech", |
| 74 | + "PySide6.QtQuick3D", |
| 75 | + "PySide6.QtVirtualKeyboard", |
| 76 | + "PySide6.QtGraphs", |
| 77 | + "PySide6.Qt5Compat", |
| 78 | + # ── Other unused stdlib modules ── |
| 79 | + "unittest", |
| 80 | + "xmlrpc", |
| 81 | + "pydoc", |
| 82 | + "doctest", |
| 83 | + "tkinter", |
| 84 | + ], |
| 85 | + noarchive=False, |
| 86 | +) |
| 87 | + |
| 88 | +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) |
| 89 | + |
| 90 | +# ── Filter out massive Qt DLLs and data we don't need ────────────────── |
| 91 | +# The PySide6 hooks copy EVERYTHING (WebEngine=193MB, 3D, Charts, etc.). |
| 92 | +# We only need: Core, Gui, Widgets, Qml, Quick, QuickControls2 (Material), |
| 93 | +# OpenGL, Network, ShaderTools, and a few essentials. |
| 94 | +_qt_keep = { |
| 95 | + # Core Qt |
| 96 | + "Qt6Core", "Qt6Gui", "Qt6Widgets", "Qt6Network", "Qt6OpenGL", |
| 97 | + # QML / Quick |
| 98 | + "Qt6Qml", "Qt6QmlCore", "Qt6QmlMeta", "Qt6QmlModels", |
| 99 | + "Qt6QmlNetwork", "Qt6QmlWorkerScript", |
| 100 | + "Qt6Quick", "Qt6QuickControls2", "Qt6QuickControls2Impl", |
| 101 | + "Qt6QuickControls2Basic", "Qt6QuickControls2BasicStyleImpl", |
| 102 | + "Qt6QuickControls2Material", "Qt6QuickControls2MaterialStyleImpl", |
| 103 | + "Qt6QuickTemplates2", "Qt6QuickLayouts", "Qt6QuickEffects", |
| 104 | + "Qt6QuickShapes", |
| 105 | + # Rendering |
| 106 | + "Qt6ShaderTools", |
| 107 | + # PySide6 runtime |
| 108 | + "pyside6.abi3", "pyside6qml.abi3", "shiboken6.abi3", |
| 109 | + # VC runtime |
| 110 | + "MSVCP140", "MSVCP140_1", "MSVCP140_2", |
| 111 | + "VCRUNTIME140", "VCRUNTIME140_1", |
| 112 | +} |
| 113 | + |
| 114 | +def _should_keep(name): |
| 115 | + """Return True if this binary/data entry should be kept.""" |
| 116 | + # Always keep non-PySide6 files |
| 117 | + if "PySide6" not in name and "pyside6" not in name.lower(): |
| 118 | + return True |
| 119 | + # Check the filename (last component) |
| 120 | + base = os.path.basename(name) |
| 121 | + stem = os.path.splitext(base)[0] |
| 122 | + # Keep if it's in our whitelist |
| 123 | + if stem in _qt_keep: |
| 124 | + return True |
| 125 | + # Keep all .pyd files (Python extensions — small and needed) |
| 126 | + if base.endswith(".pyd"): |
| 127 | + return True |
| 128 | + # Keep plugin dirs we need (platforms, imageformats, styles, iconengines) |
| 129 | + for keep in ("platforms", "imageformats", "styles", "iconengines", |
| 130 | + "platforminputcontexts"): |
| 131 | + if keep in name: |
| 132 | + return True |
| 133 | + # Keep QML dirs we need |
| 134 | + for keep_qml in ("QtCore", "QtQml", "QtQuick", "QtNetwork"): |
| 135 | + pat = os.path.join("qml", keep_qml) |
| 136 | + if pat in name.replace("/", os.sep): |
| 137 | + return True |
| 138 | + # Drop everything else (WebEngine, 3D, Charts, Multimedia, etc.) |
| 139 | + return False |
| 140 | + |
| 141 | +a.binaries = [b for b in a.binaries if _should_keep(b[0])] |
| 142 | +a.datas = [d for d in a.datas if _should_keep(d[0])] |
| 143 | + |
| 144 | +exe = EXE( |
| 145 | + pyz, |
| 146 | + a.scripts, |
| 147 | + [], # not one-file (faster startup, easier debugging) |
| 148 | + exclude_binaries=True, |
| 149 | + name="LogiControl", |
| 150 | + debug=False, |
| 151 | + bootloader_ignore_signals=False, |
| 152 | + strip=False, |
| 153 | + upx=False, # UPX OFF — decompression at startup is very slow |
| 154 | + console=False, # windowed app (no terminal) |
| 155 | + icon=os.path.join(ROOT, "images", "logo.ico"), |
| 156 | + uac_admin=False, # does NOT require admin |
| 157 | +) |
| 158 | + |
| 159 | +coll = COLLECT( |
| 160 | + exe, |
| 161 | + a.binaries, |
| 162 | + a.zipfiles, |
| 163 | + a.datas, |
| 164 | + strip=False, |
| 165 | + upx=False, # UPX OFF — faster cold start |
| 166 | + upx_exclude=[], |
| 167 | + name="LogiControl", |
| 168 | +) |
| 169 | + |
| 170 | +# ── Post-build cleanup: remove Qt QML/plugin dirs we don't need ────────── |
| 171 | +# PyInstaller's hooks copy the entire PySide6 QML tree; we only need |
| 172 | +# QtQuick/Controls + Material, QtQml, QtQuick/Layouts, QtQuick/Templates, |
| 173 | +# QtQuick/Window. Everything else is dead weight that slows startup. |
| 174 | +_dist = os.path.join("dist", "LogiControl", "_internal", "PySide6") |
| 175 | + |
| 176 | +# QML dirs to KEEP (everything else under qml/ is deleted) |
| 177 | +_keep_qml = { |
| 178 | + "QtCore", "QtQml", "QtQuick", "QtNetwork", |
| 179 | +} |
| 180 | + |
| 181 | +# Under QtQuick, keep only what the app uses |
| 182 | +_keep_qtquick = { |
| 183 | + "Controls", "Layouts", "Templates", "Window", |
| 184 | +} |
| 185 | + |
| 186 | +# Plugin dirs to KEEP |
| 187 | +_keep_plugins = { |
| 188 | + "iconengines", "imageformats", "platforms", |
| 189 | + "platforminputcontexts", "styles", |
| 190 | +} |
| 191 | + |
| 192 | +def _cleanup(): |
| 193 | + qml_root = os.path.join(_dist, "qml") |
| 194 | + if os.path.isdir(qml_root): |
| 195 | + for d in os.listdir(qml_root): |
| 196 | + if d not in _keep_qml: |
| 197 | + p = os.path.join(qml_root, d) |
| 198 | + if os.path.isdir(p): |
| 199 | + shutil.rmtree(p, ignore_errors=True) |
| 200 | + print(f" [cleanup] removed qml/{d}") |
| 201 | + |
| 202 | + # Trim inside QtQuick |
| 203 | + qtquick = os.path.join(qml_root, "QtQuick") |
| 204 | + if os.path.isdir(qtquick): |
| 205 | + for d in os.listdir(qtquick): |
| 206 | + if d not in _keep_qtquick: |
| 207 | + p = os.path.join(qtquick, d) |
| 208 | + if os.path.isdir(p): |
| 209 | + shutil.rmtree(p, ignore_errors=True) |
| 210 | + print(f" [cleanup] removed qml/QtQuick/{d}") |
| 211 | + |
| 212 | + plugins_root = os.path.join(_dist, "plugins") |
| 213 | + if os.path.isdir(plugins_root): |
| 214 | + for d in os.listdir(plugins_root): |
| 215 | + if d not in _keep_plugins: |
| 216 | + p = os.path.join(plugins_root, d) |
| 217 | + if os.path.isdir(p): |
| 218 | + shutil.rmtree(p, ignore_errors=True) |
| 219 | + print(f" [cleanup] removed plugins/{d}") |
| 220 | + |
| 221 | + # Remove translations (not needed) |
| 222 | + trans = os.path.join(_dist, "translations") |
| 223 | + if os.path.isdir(trans): |
| 224 | + shutil.rmtree(trans, ignore_errors=True) |
| 225 | + print(" [cleanup] removed translations/") |
| 226 | + |
| 227 | +print("[LogiControl] Post-build cleanup...") |
| 228 | +_cleanup() |
| 229 | +print("[LogiControl] Cleanup done.") |
0 commit comments