* pyproject:wheel 打三个包(sqlite_vfs + svfs_studio + svfs_client)——两个桌面 应用的 console script 都声明在同一个发行版里,只打核心包会让 pip 装完 `svfs-client` 命令存在而模块缺失(跑起来是 ImportError);extras 拆成 client(完整 PySide6,WebEngine 必需)/ studio(PySide6-Essentials 够用)/ gui(两者合集,保留旧写法)。实测重建 wheel 三个包齐全、纯标准库层在无 PySide6 的环境里可导入; * packaging/:PyInstaller spec(白名单裁剪 Qt 体积,DevTools/QML/opengl32sw 的 裁剪依据都记在注释里)与 build.py 编排;Nuitka 路线的已知问题(编译后 Shiboken 无法导入)写在脚本注释里,推荐走 PyInstaller; * .github/workflows/tests.yml:windows + ubuntu 双跑。文件头注明了两个尚未在真 runner 上验证过的点——依赖索引(pyproject 默认指清华镜像,CI 里用 UV_DEFAULT_INDEX 覆盖回 PyPI)与 Linux 上 Qt 需要的系统库; * README:按实现重新对齐——登录与认证(凭据存储、会话失效反馈、离线宽限)、 可靠性与诊断(客户端自更新、网络超时/重试/代理/CA、日志落盘)、plugin.json 的 net_hosts、数据目录(credentials.dat / logs)、桥 API、安装方式与 extras。 uv.lock 同步(dev 组与 extras)。
143 lines
4.9 KiB
RPMSpec
143 lines
4.9 KiB
RPMSpec
# -*- mode: python ; coding: utf-8 -*-
|
||
"""PyInstaller 打包配置:SVFS Client(PySide6 + QtWebEngine 插件客户端)。
|
||
|
||
用法(或直接用 packaging/build.py 编排)::
|
||
|
||
uv sync --extra gui
|
||
uv pip install pyinstaller
|
||
uv run pyinstaller --noconfirm packaging/svfs_client.spec
|
||
# 目录版产物: dist/SVFS-Client/SVFS-Client.exe
|
||
|
||
单文件版(QtWebEngine 启动前要先解包到临时目录,明显更慢)::
|
||
|
||
SVFS_ONEFILE=1 uv run pyinstaller --noconfirm packaging/svfs_client.spec
|
||
|
||
QtWebEngine 的资源(icudtl.dat、qtwebengine_resources.pak、locale、
|
||
QtWebEngineProcess.exe)由 pyinstaller-hooks-contrib 的 PySide6 钩子自动收集,
|
||
本 spec 不需要手工列——但要保证 PyInstaller >= 6。
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
|
||
from PyInstaller.utils.hooks import collect_submodules
|
||
|
||
# spec 文件在 <repo>/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_client.py")],
|
||
pathex=[SRC],
|
||
binaries=[],
|
||
datas=[],
|
||
hiddenimports=[
|
||
# QtWebEngine 链条显式列出,确保 WebEngine 钩子必定触发
|
||
"PySide6.QtWebEngineWidgets",
|
||
"PySide6.QtWebEngineCore",
|
||
"PySide6.QtWebChannel",
|
||
*collect_submodules("svfs_client"),
|
||
*collect_submodules("sqlite_vfs"),
|
||
],
|
||
hookspath=[],
|
||
hooksconfig={},
|
||
runtime_hooks=[],
|
||
excludes=["tkinter"],
|
||
noarchive=False,
|
||
)
|
||
|
||
# ----------------------------------------------------------------------
|
||
# 体积裁剪(白名单制):PySide6 的钩子会把 wheel 里**所有** Qt 模块的
|
||
# DLL/pyd 整包搬进来(Quick3D 全家、QuickControls2、Pdf、OpenGL、Svg……
|
||
# 约 70M 的没人用的东西),黑名单删不干净,这里只留依赖链上的:
|
||
#
|
||
# * Qt6WebEngineCore 实测导入(objdump):Core/Gui/Network/Positioning/Qml/Quick/
|
||
# WebChannel;WebEngineWidgets 另需 PrintSupport/QuickWidgets/Widgets;
|
||
# Qt6Quick 另需 Qt6OpenGL 与 QmlMeta/QmlModels —— 这些必须留。
|
||
# * pyd 只留 Python 层真实用到的(实测 import 链):QtCore/QtGui/QtWidgets/
|
||
# QtNetwork/QtPrintSupport/QtWebChannel/QtWebEngineCore/QtWebEngineWidgets;
|
||
# 其余绑定(Qml/Quick/QuickWidgets/Positioning/OpenGL)没有 Python 调用者,
|
||
# 它们的 DLL 由上面的 Qt6 白名单保留。
|
||
# * opengl32sw.dll(20M 软件渲染回退)删:Chromium 自带 SwiftShader,
|
||
# 冻结包深度冒烟(--open 加载页面)通过。
|
||
# * DevTools 资源包(85M)、QML 组件目录(29M)、译文、虚拟键盘、qmltooling:
|
||
# 生产分发用不到(DevTools 代价:插件页面里检查元素无资源,调试用开发环境)。
|
||
# 裁不动:Qt6WebEngineCore.dll ~195M(Chromium 本体)与 icudtl.dat 10M。
|
||
# ----------------------------------------------------------------------
|
||
KEEP_QT6_DLLS = {
|
||
"Qt6Core", "Qt6Gui", "Qt6Widgets", "Qt6Network", "Qt6Positioning",
|
||
"Qt6Qml", "Qt6QmlMeta", "Qt6QmlModels", "Qt6QmlWorkerScript",
|
||
"Qt6Quick", "Qt6OpenGL", "Qt6WebChannel", "Qt6PrintSupport",
|
||
"Qt6QuickWidgets", "Qt6WebEngineCore", "Qt6WebEngineWidgets",
|
||
}
|
||
KEEP_PYDS = {"QtCore", "QtGui", "QtWidgets", "QtNetwork", "QtPrintSupport",
|
||
"QtWebChannel", "QtWebEngineCore", "QtWebEngineWidgets"}
|
||
|
||
QT_DLL_RE = re.compile(r"PySide6[/\\](Qt6[^/\\]*\.dll)$", re.IGNORECASE)
|
||
PYD_RE = re.compile(r"PySide6[/\\]([^/\\]*\.pyd)$", re.IGNORECASE)
|
||
DROP_CLIENT = re.compile(
|
||
r"PySide6[/\\](?:"
|
||
r"resources[/\\]qtwebengine_devtools_resources[^/\\]*\.pak"
|
||
r"|qml[/\\]"
|
||
r"|translations[/\\]"
|
||
r"|plugins[/\\]virtualkeyboard[/\\]"
|
||
r"|plugins[/\\]qmltooling[/\\]"
|
||
r"|opengl32sw\.dll"
|
||
r")",
|
||
re.IGNORECASE,
|
||
)
|
||
|
||
|
||
def _keep(entry_name):
|
||
if DROP_CLIENT.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-Client",
|
||
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-Client",
|
||
)
|