复制 local 代码到 serve
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
"""PyInstaller 打包配置 — 桌面客户端 (onedir 模式)。
|
||||
|
||||
为什么 onedir 而非 onefile:
|
||||
- onefile 每次启动都解压到临时 _MEIxxxxx, 与 APScheduler/多线程冲突
|
||||
- onedir 启动更快, 调试更方便 (可看到目录结构), 原生库直接在目录里
|
||||
- 体积差异通过压缩安装包弥补 (CI 里 zip 打包)
|
||||
|
||||
入口: backend/app/desktop.py (桌面版入口, 含 uvicorn + pywebview)
|
||||
|
||||
构建 (在项目根目录):
|
||||
cd frontend && pnpm build # 先构建前端到 frontend/dist
|
||||
pyinstaller packaging/tickflow.spec # 产物在 dist/TickFlowStockPanel/
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PyInstaller.utils.hooks import (
|
||||
collect_all,
|
||||
collect_submodules,
|
||||
collect_data_files,
|
||||
copy_metadata,
|
||||
)
|
||||
|
||||
_IS_MACOS = sys.platform == "darwin"
|
||||
|
||||
block_cipher = None
|
||||
|
||||
# ── 资源路径基准: 项目根 (spec 文件在 packaging/ 下) ──────────────────
|
||||
ROOT = Path(SPECPATH).parent
|
||||
FRONTEND_DIST = str(ROOT / "frontend" / "dist")
|
||||
TIERS_YAML = str(ROOT / "tiers.yaml")
|
||||
BUILTIN_STRATEGIES = str(ROOT / "backend" / "app" / "strategy" / "builtin")
|
||||
# 图标按平台选: Windows 用 .ico, macOS 用 .icns (PyInstaller 对 .ico 在
|
||||
# mac 上静默忽略, 不换格式 Dock/Finder 会显示通用图标)。两者都由
|
||||
# packaging/generate_icon.py 一并生成。
|
||||
APP_ICON = str(ROOT / "packaging" / ("icon.icns" if _IS_MACOS else "icon.ico"))
|
||||
|
||||
# ── 收集带原生库的依赖 (.libs/ 目录必须完整, 否则启动崩) ─────────────
|
||||
# polars / pyarrow / duckdb / fastexcel 都自带共享库子目录
|
||||
datas = []
|
||||
binaries = []
|
||||
hiddenimports = []
|
||||
|
||||
for pkg in ("polars", "pyarrow", "duckdb", "fastexcel"):
|
||||
d, b, h = collect_all(pkg)
|
||||
datas += d
|
||||
binaries += b
|
||||
hiddenimports += h
|
||||
|
||||
# polars-runtime-32 (rtcompat 兼容内核): release.yml 用 --extra legacy-cpu 安装。
|
||||
# 它是独立的伴侣二进制包 (含 .pyd/.so), 与 polars 主包分开发布,
|
||||
# collect_all("polars") 抓不到它的目录 —— 必须显式收集, 否则老 CPU 用户
|
||||
# 运行时 rtcompat 加载器找不到兼容库仍会崩 (Illegal instruction)。
|
||||
# 不存在时 (未装 legacy-cpu) collect_all 返回空, 不影响普通构建。
|
||||
try:
|
||||
rt_d, rt_b, rt_h = collect_all("polars_runtime_32")
|
||||
datas += rt_d
|
||||
binaries += rt_b
|
||||
hiddenimports += rt_h
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# polars 新 ABI 运行时目录 (_polars_runtime_32) 需显式收集子模块
|
||||
hiddenimports += collect_submodules("polars")
|
||||
|
||||
# ── pywebview 平台后端 (动态导入, PyInstaller 默认抓不到) ────────────
|
||||
hiddenimports += collect_submodules("webview")
|
||||
hiddenimports += collect_submodules("webview.platforms")
|
||||
|
||||
# ── 系统通知后端 (winotify/plyer 按平台动态导入) ─────────────────────
|
||||
if sys.platform == "win32":
|
||||
hiddenimports += collect_submodules("winotify")
|
||||
hiddenimports += collect_submodules("plyer")
|
||||
hiddenimports += collect_submodules("plyer.platforms")
|
||||
|
||||
# ── uvicorn 动态导入的模块 (loop/protocol/logging 按字符串加载) ──────
|
||||
hiddenimports += [
|
||||
"uvicorn.logging",
|
||||
"uvicorn.loops",
|
||||
"uvicorn.loops.auto",
|
||||
"uvicorn.loops.asyncio",
|
||||
"uvicorn.protocols",
|
||||
"uvicorn.protocols.http",
|
||||
"uvicorn.protocols.http.auto",
|
||||
"uvicorn.protocols.http.h11_impl",
|
||||
"uvicorn.protocols.websockets",
|
||||
"uvicorn.protocols.websockets.auto",
|
||||
"uvicorn.lifespan",
|
||||
"uvicorn.lifespan.on",
|
||||
]
|
||||
|
||||
# ── fastapi / pydantic 元数据 (版本检测用) ───────────────────────────
|
||||
# 注意: 任何用 importlib.metadata.version() 读版本的包, 都必须 copy_metadata,
|
||||
# 否则 frozen 后报 PackageNotFoundError。tickflow 包内部就是这么读的。
|
||||
# 用容错写法: 不存在的包跳过, 避免不同环境 (有无装某依赖) 导致构建失败。
|
||||
def _safe_metadata(pkg):
|
||||
"""收集包元数据, 包不存在时静默跳过。"""
|
||||
try:
|
||||
return copy_metadata(pkg)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
for pkg in (
|
||||
"fastapi", "pydantic", "pydantic_settings", "starlette", "anyio",
|
||||
"tickflow", # tickflow/__version__.py 用 importlib.metadata 读版本
|
||||
"uvicorn", "polars", "duckdb", "pyarrow", "httpx", "numpy", "pandas",
|
||||
"openai", "platformdirs", "winotify", "plyer", "apscheduler",
|
||||
"python-dotenv", "fastexcel",
|
||||
):
|
||||
datas += _safe_metadata(pkg)
|
||||
|
||||
# ── 随包资源 (只读, 放进 _MEIPASS) ────────────────────────────────────
|
||||
# 前端 dist → static/ (config.py frozen 模式读 _MEIPASS/static)
|
||||
datas += [(FRONTEND_DIST, "static")]
|
||||
# tiers.yaml → 包根 (config.py frozen 模式读 _MEIPASS/tiers.yaml)
|
||||
datas += [(TIERS_YAML, ".")]
|
||||
# 内置策略 → app/strategy/builtin/ (importlib 动态加载, 不能进 PYZ)
|
||||
datas += [(BUILTIN_STRATEGIES, "app/strategy/builtin")]
|
||||
|
||||
# ── 排除不需要的重型依赖 (主包不含 vectorbt 回测链) ──────────────────
|
||||
excludes = [
|
||||
"vectorbt",
|
||||
"numba",
|
||||
"llvmlite",
|
||||
"matplotlib",
|
||||
"plotly",
|
||||
"ipywidgets",
|
||||
"nbformat",
|
||||
"nbconvert",
|
||||
"jupyter",
|
||||
"IPython",
|
||||
"pytest",
|
||||
"pytest_asyncio",
|
||||
"ruff",
|
||||
"mypy",
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
[str(ROOT / "backend" / "app" / "desktop.py")],
|
||||
pathex=[str(ROOT / "backend")],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=excludes,
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="TickFlowStockPanel",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False, # UPX 压缩原生库常导致崩溃, 关闭
|
||||
console=False, # 桌面应用: 不显示控制台窗口 (调试时临时改 True 抓日志)
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=APP_ICON, # 应用图标 (与 favicon/logo 一致)
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
name="TickFlowStockPanel",
|
||||
)
|
||||
|
||||
# ── macOS: 封装成 .app 包 ────────────────────────────────────────────
|
||||
# Windows/Linux: 上面 COLLECT 产出的 onedir 目录即是最终产物。
|
||||
# macOS: 额外加 BUNDLE, 把目录包装成标准 .app (Contents/MacOS/...),
|
||||
# 这样 Dock/Finder 能识别, 用户可双击启动, 并显示自定义 .icns 图标。
|
||||
# BUNDLE 必须引用上面的 COLLECT (coll), 它会把 coll 的产物搬进 .app。
|
||||
#
|
||||
# PyInstaller 6 BUNDLE 关键参数 (见 building/osx.py):
|
||||
# - version: → CFBundleShortVersionString (默认 0.0.0, 必须显式传)
|
||||
# - info_plist (单数!): 用户自定义键, update 合并进默认 plist, 可覆盖任意字段
|
||||
if _IS_MACOS:
|
||||
import json
|
||||
|
||||
# 版本号从 frontend/package.json 读, 与 Release tag 对齐
|
||||
_pkg_json = json.loads((ROOT / "frontend" / "package.json").read_text(encoding="utf-8"))
|
||||
APP_VERSION = _pkg_json["version"]
|
||||
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name="TickFlowStockPanel.app",
|
||||
icon=APP_ICON,
|
||||
bundle_identifier="com.tickflow.stockpanel",
|
||||
version=APP_VERSION, # → CFBundleShortVersionString / CFBundleVersion
|
||||
info_plist={
|
||||
"CFBundleName": "TickFlow Stock Panel",
|
||||
"CFBundleDisplayName": "TickFlow 股票面板",
|
||||
"CFBundleVersion": APP_VERSION,
|
||||
"NSHighResolutionCapable": True,
|
||||
"LSMinimumSystemVersion": "10.13",
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user