mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 14:24:15 +08:00
fix(packaging): support legacy Polars CPUs
This commit is contained in:
+3
-2
@@ -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 =====
|
||||
|
||||
@@ -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 产出封装成单文件安装包
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 默认抓不到) ────────────
|
||||
|
||||
Reference in New Issue
Block a user