fix(packaging): support legacy Polars CPUs

This commit is contained in:
shy3130
2026-07-12 14:08:24 +08:00
parent 2014da57a9
commit a6e9987be6
7 changed files with 91 additions and 27 deletions
+3 -2
View File
@@ -20,8 +20,9 @@ LOG_LEVEL=INFO
# 建议至少 6 位。.env 文件权限保持 600 且不要提交到 Git。
AUTH_PASSWORD=
# Optional Docker build extras. Set to legacy-cpu on older VPS hosts
# without AVX2/FMA support.
# Optional backend dependency extras for Docker and ./dev.sh / .\dev.ps1.
# Set to legacy-cpu on older CPUs without AVX2/FMA support.
# Combine with backtest as: legacy-cpu backtest
BACKEND_EXTRAS=
# ===== Data =====
+17 -1
View File
@@ -127,7 +127,7 @@ jobs:
uv run python ../packaging/generate_icon.py
- name: 打包 (PyInstaller)
# 在 backend 目录跑 (用该目录的 venv: pywebview/polars 等)。
# 在 backend 目录跑 (用该目录的 venv: pywebview/polars 等依赖)。
# 产物输出到 backend/dist/ (PyInstaller 默认路径), 与 packaging/tickflow.iss
# 第 78 行的 Source 路径 (..\backend\dist\TickFlowStockPanel\*) 保持一致。
# SPECPATH 由 spec 文件位置决定 (packaging/tickflow.spec → 项目根),
@@ -135,6 +135,22 @@ jobs:
working-directory: backend
run: uv run pyinstaller ../packaging/tickflow.spec --noconfirm
- name: 检查 Polars 兼容内核 (仅 Windows)
if: matrix.platform == 'windows'
shell: pwsh
run: |
$runtimeRoot = 'backend/dist/TickFlowStockPanel/_internal'
$required = @(
'_polars_runtime_32/_polars_runtime.pyd',
'_polars_runtime_compat/_polars_runtime.pyd'
)
$missing = $required | Where-Object {
-not (Test-Path (Join-Path $runtimeRoot $_))
}
if ($missing) {
throw "PyInstaller output is missing Polars runtime(s): $($missing -join ', ')"
}
- name: 安装 Inno Setup (仅 Windows)
if: matrix.platform == 'windows'
# Inno Setup 6: 把 PyInstaller 产出封装成单文件安装包
+30 -4
View File
@@ -108,11 +108,37 @@ function Free-Port($name, $port) {
Free-Port 'backend' $BackendPort
Free-Port 'frontend' $FrontendPort
# ===== 3. First-time dependency install =====
if (-not (Test-Path (Join-Path $BackendDir '.venv'))) {
Log-Info 'first run - installing Python deps (1-2 min)...'
# ===== 3. Dependency install =====
# Match Docker's whitespace-separated BACKEND_EXTRAS behavior so old CPUs can
# select Polars' rtcompat runtime before the backend starts.
$BackendExtras = $env:BACKEND_EXTRAS
if (-not (Test-Path Env:BACKEND_EXTRAS)) {
$envFile = Join-Path $Root '.env'
if (Test-Path $envFile) {
foreach ($line in Get-Content $envFile) {
if ($line -match '^\s*BACKEND_EXTRAS\s*=\s*(.*?)\s*$') {
$BackendExtras = $Matches[1]
break
}
}
}
}
$BackendExtraArgs = @()
if (-not [string]::IsNullOrWhiteSpace($BackendExtras)) {
foreach ($extra in ($BackendExtras -split '\s+' | Where-Object { $_ })) {
$BackendExtraArgs += '--extra', $extra
}
}
if (-not (Test-Path (Join-Path $BackendDir '.venv')) -or $BackendExtraArgs.Count) {
if ($BackendExtraArgs.Count) {
Log-Info "syncing Python deps with extras: $BackendExtras"
} else {
Log-Info 'first run - installing Python deps (1-2 min)...'
}
Push-Location $BackendDir
try { & uv sync } finally { Pop-Location }
try { & uv sync @BackendExtraArgs } finally { Pop-Location }
if ($LASTEXITCODE -ne 0) { Log-Err 'uv sync failed'; exit 1 }
Log-Ok 'backend deps installed'
}
+22 -4
View File
@@ -16,6 +16,20 @@ FRONTEND_DIR="$ROOT/frontend"
BACKEND_PORT="${BACKEND_PORT:-3018}"
FRONTEND_PORT="${FRONTEND_PORT:-3011}"
# Match Docker's BACKEND_EXTRAS behavior so old CPUs can select Polars'
# rtcompat runtime before the backend starts. An exported value wins over .env.
if [[ -z "${BACKEND_EXTRAS+x}" && -f "$ROOT/.env" ]]; then
BACKEND_EXTRAS="$(awk '/^[[:space:]]*BACKEND_EXTRAS[[:space:]]*=/ {sub(/^[^=]*=/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit}' "$ROOT/.env")"
fi
BACKEND_EXTRAS="${BACKEND_EXTRAS:-}"
BACKEND_EXTRA_ARGS=()
if [[ -n "$BACKEND_EXTRAS" ]]; then
read -r -a backend_extras <<< "$BACKEND_EXTRAS"
for extra in "${backend_extras[@]}"; do
BACKEND_EXTRA_ARGS+=(--extra "$extra")
done
fi
BLUE='\033[0;34m'
GREEN='\033[0;32m'
RED='\033[0;31m'
@@ -71,10 +85,14 @@ free_port() {
free_port backend "$BACKEND_PORT"
free_port frontend "$FRONTEND_PORT"
# ===== 3. 首次依赖安装 =====
if [ ! -d "$BACKEND_DIR/.venv" ]; then
info "后端首次启动 — 安装 Python 依赖(约 1-2 分钟)..."
( cd "$BACKEND_DIR" && uv sync )
# ===== 3. 依赖安装 =====
if [ ! -d "$BACKEND_DIR/.venv" ] || [ "${#BACKEND_EXTRA_ARGS[@]}" -gt 0 ]; then
if [ "${#BACKEND_EXTRA_ARGS[@]}" -gt 0 ]; then
info "同步后端 Python 依赖,extras: $BACKEND_EXTRAS"
else
info "后端首次启动 — 安装 Python 依赖(约 1-2 分钟)..."
fi
( cd "$BACKEND_DIR" && uv sync "${BACKEND_EXTRA_ARGS[@]}" )
ok "后端依赖装好了"
fi
+2 -2
View File
@@ -93,13 +93,13 @@ AUTH_PASSWORD=你的密码 # 至少 6 位;仅首次生效,已设过则不覆
---
## Docker 构建 Extras(可选)
## 后端依赖 Extras(可选)
```ini
BACKEND_EXTRAS= # 留空默认;legacy-cpu 兼容老 CPU
```
VPS 无 AVX2/FMA 支持时设为 `legacy-cpu`,会给 Polars 切到 `rtcompat` 运行时;需回测则 `legacy-cpu backtest`。详见 [deployment.md → 老 CPU 兼容](./deployment.md#老-cpu-兼容avx2fma-缺失)。
CPU 无 AVX2/FMA 支持时设为 `legacy-cpu`,会给 Polars 切到 `rtcompat` 运行时;需回测则 `legacy-cpu backtest`Docker 构建和 `./dev.sh` / `.\dev.ps1` 都会读取此值并同步依赖。详见 [deployment.md → 老 CPU 兼容](./deployment.md#老-cpu-兼容avx2fma-缺失)。
---
+6 -1
View File
@@ -27,6 +27,8 @@ cp .env.example .env # 按需填 TICKFLOW_API_KEY(留空 = None 模式)
```bash
# 后端
cd backend && uv sync --extra backtest # 含回测依赖
# 老 CPU: uv sync --extra legacy-cpu
# 老 CPU + 回测: uv sync --extra legacy-cpu --extra backtest
uv run uvicorn app.main:app --reload --port 3018
# 前端
@@ -69,13 +71,16 @@ Fork 本仓库后,手动触发 [Release 打包工作流](https://github.com/shy3
如果运行时报 `avx2`/`fma` 缺失,或进程 `exit 132`,说明 CPU 不支持 AVX2 指令集(常见于老 VPS)。解决:
- **桌面客户端**:安装包已内置兼容内核,新老 CPU 通吃
- **Docker / 源码**:在 `.env` 打开 `BACKEND_EXTRAS=legacy-cpu` 后重建,会给 Polars 切到 `rtcompat` 运行时
- **Dev 源码启动**:在根目录 `.env` 设置后运行 `./dev.sh` 或 Windows 的 `.\dev.ps1`;即使已有 `.venv`,启动器也会同步兼容内核
- **Docker**:在根目录 `.env` 设置后执行 `docker compose up --build`
```ini
BACKEND_EXTRAS=legacy-cpu # 兼容老 CPU
BACKEND_EXTRAS=legacy-cpu backtest # 兼容老 CPU + 回测依赖
```
手动启动源码时,也可以在 `backend/` 目录直接执行 `uv sync --extra legacy-cpu`。不要设置 `POLARS_SKIP_CPU_CHECK`,它只会隐藏警告,实际执行不支持的指令时仍可能崩溃。
### 回测依赖说明
vectorbt → numba 体积较大,作为可选 extras(`uv sync --extra backtest`)。macOS / Intel 无预构建 wheel 时需 `brew install cmake` 现场编译。
+11 -13
View File
@@ -13,6 +13,7 @@
pyinstaller packaging/tickflow.spec # 产物在 dist/TickFlowStockPanel/
"""
import sys
from importlib.util import find_spec
from pathlib import Path
from PyInstaller.utils.hooks import (
@@ -48,20 +49,17 @@ for pkg in ("polars", "pyarrow", "duckdb", "fastexcel"):
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 的发行包名为 polars-runtime-32 / polars-runtime-compat, 但实际
# Python 导入包带前导下划线。release.yml 安装 legacy-cpu 后必须收集二者,
# 否则 onedir 产物无法在没有 AVX2/FMA 的旧 CPU 上加载兼容内核。
for pkg in ("_polars_runtime_32", "_polars_runtime_compat"):
if find_spec(pkg) is not None:
rt_d, rt_b, rt_h = collect_all(pkg)
datas += rt_d
binaries += rt_b
hiddenimports += rt_h
# polars 新 ABI 运行时目录 (_polars_runtime_32) 需显式收集子模块
# Polars 新 ABI 运行时由加载器选择,需显式收集子模块
hiddenimports += collect_submodules("polars")
# ── pywebview 平台后端 (动态导入, PyInstaller 默认抓不到) ────────────