chore: 三包一起分发、桌面应用打包脚本与 CI,README 对齐实现
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

* 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)。
This commit is contained in:
2026-09-16 17:15:55 +08:00
parent f62704ca0e
commit ae9fc8dbe4
10 changed files with 1815 additions and 52 deletions
+126
View File
@@ -0,0 +1,126 @@
# -*- 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 文件在 <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_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_<key>) 取视图工厂,收集整个包最稳妥
*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",
)