Files
svfs/packaging/svfs_client.spec
T
chakcy ae9fc8dbe4
tests / ubuntu-latest / py3.13 (push) Waiting to run
tests / windows-latest / py3.13 (push) Waiting to run
tests / ubuntu-latest / py3.13 (pull_request) Has been cancelled
tests / windows-latest / py3.13 (pull_request) Has been cancelled
chore: 三包一起分发、桌面应用打包脚本与 CI,README 对齐实现
* 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)。
2026-09-16 17:15:55 +08:00

143 lines
4.9 KiB
RPMSpec
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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",
)