# -*- mode: python ; coding: utf-8 -*- """PyInstaller 打包配置:SVFS Studio(PySide6 桌面工具,无 WebEngine)。 用法(或直接用 packaging/build.py 编排):: uv sync --extra gui uv pip install pyinstaller uv run pyinstaller --noconfirm packaging/svfs_studio.spec # 目录版产物: dist/SVFS-Studio/SVFS-Studio.exe 单文件版(启动稍慢,Qt 会先解包到临时目录):: SVFS_ONEFILE=1 uv run pyinstaller --noconfirm packaging/svfs_studio.spec """ import os import re from PyInstaller.utils.hooks import collect_submodules # spec 文件在 /packaging/ 下,仓库根与 src 由此反推 REPO = os.path.dirname(os.path.dirname(os.path.abspath(SPEC))) SRC = os.path.join(REPO, "src") ONEFILE = os.environ.get("SVFS_ONEFILE", "") == "1" ICON = os.path.join(REPO, "resources", "ico", "logo.ico") a = Analysis( [os.path.join(REPO, "packaging", "run_studio.py")], pathex=[SRC], binaries=[], datas=[ # app_icon() / repo_root() 在冻结环境下从包内找 resources/(见 sample.py) (os.path.join(REPO, "resources", "ico"), "resources/ico"), (os.path.join(REPO, "resources", "images"), "resources/images"), ], hiddenimports=[ # 界面里通过 getattr(build_) 取视图工厂,收集整个包最稳妥 *collect_submodules("svfs_studio"), *collect_submodules("sqlite_vfs"), ], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=["tkinter"], noarchive=False, ) # ---------------------------------------------------------------------- # 体积裁剪(白名单制):PySide6 的钩子会把 wheel 里**所有** Qt 模块的 # DLL/pyd 整包搬进来,而本应用只用 QtCore/QtGui/QtWidgets: # * 未使用的 Qt 模块 DLL(Qml/Quick/Quick3D/Pdf/Network/OpenGL/Svg/ # VirtualKeyboard/ShaderTools…,约 25M) # * 未使用的 python 绑定 pyd # * opengl32sw.dll(20M 软件渲染回退;Widgets 走光栅绘制用不到, # 无显卡的机器出问题就把它加回 KEEP 外的例外) # * Qt 自带译文(*.qm,6.7M;界面文案全部硬编码中文) # * 依赖被删模块的插件(networkinformation/tls/qsvg) # ---------------------------------------------------------------------- KEEP_QT6_DLLS = {"Qt6Core", "Qt6Gui", "Qt6Widgets"} KEEP_PYDS = {"QtCore", "QtGui", "QtWidgets"} QT_DLL_RE = re.compile(r"PySide6[/\\](Qt6[^/\\]*\.dll)$", re.IGNORECASE) PYD_RE = re.compile(r"PySide6[/\\]([^/\\]*\.pyd)$", re.IGNORECASE) DROP_STUDIO = re.compile( r"PySide6[/\\](?:" r"opengl32sw\.dll" r"|d3dcompiler_47\.dll" r"|translations[/\\]" r"|plugins[/\\](?:networkinformation|tls)[/\\]" r"|plugins[/\\]imageformats[/\\]qsvg\.dll" r"|plugins[/\\]iconengines[/\\]qsvgicon\.dll" r")", re.IGNORECASE, ) def _keep(entry_name): if DROP_STUDIO.search(entry_name): return False match = QT_DLL_RE.search(entry_name) if match: return match.group(1).rsplit(".", 1)[0] in KEEP_QT6_DLLS match = PYD_RE.search(entry_name) if match: return match.group(1).rsplit(".", 1)[0] in KEEP_PYDS return True a.binaries = [entry for entry in a.binaries if _keep(entry[0])] a.datas = [entry for entry in a.datas if _keep(entry[0])] pyz = PYZ(a.pure) exe_kwargs = dict( name="SVFS-Studio", console=False, # GUI 程序,不弹控制台 icon=ICON if os.path.exists(ICON) else None, upx=False, ) if ONEFILE: exe = EXE( pyz, a.scripts, a.binaries, a.datas, [], **exe_kwargs, ) else: exe = EXE( pyz, a.scripts, [], exclude_binaries=True, **exe_kwargs, ) coll = COLLECT( exe, a.binaries, a.datas, strip=False, upx=False, name="SVFS-Studio", )