build: add pyinstaller packaging
Tests / Python 3.11 / Windows (push) Has been cancelled

This commit is contained in:
chengma
2026-07-03 09:26:27 +08:00
parent dd2a8e3360
commit 6d4f60fcdb
12 changed files with 309 additions and 12 deletions
+6
View File
@@ -37,3 +37,9 @@ env/
.idea/
.DS_Store
Thumbs.db
# ── T-524 打包 / 本地提示词产物 ──
db.sqlite
prompts/
title_prompt.txt
build/
dist/
+55
View File
@@ -0,0 +1,55 @@
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec for cmshopee Windows onedir builds.
Local user data is intentionally not bundled. The release package must not
include config files, SQLite databases, Chrome profiles, generated images,
logs, prompts, or other operator data.
"""
block_cipher = None
a = Analysis(
["main.py"],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[
"PySide6.QtCore",
"PySide6.QtGui",
"PySide6.QtWidgets",
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
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="cmshopee",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name="cmshopee",
)
+5
View File
@@ -24,6 +24,7 @@
| 运行日志 | SQLite `run_logs` / `run_log_events` | 已定 | ③ dry-run 与真实更新都留痕;结构化内容走脱敏 |
| 图片处理 | `requests`(下载)+ `Pillow`(按分辨率/jpg质量存盘) | 部分待定 | 下载旧封面;新封面按 resolution 生成、jpg_quality 存盘 |
| 测试 | `python -m compileall app main.py` + `unittest` + 手动 CDP/AI 验证 | 已定(分层) | 配置/DB/Excel/prompts 用单测;CDP/Shopee 与真实 AI 属集成验证或 mock |
| 打包分发 | PyInstaller onedir(`cmshopee.spec`) | 已定 | 产出 Windows 免安装文件夹;不内置配置、DB、图片、日志、Chrome 登录态或提示词等本地数据 |
## 二、决策记录与演进
@@ -40,6 +41,7 @@
- **T-504 更新执行增强**:③ 支持 dry-run 预览、运行日志和按账号并行;默认 dry-run 关闭、并行关闭,不引入新依赖。
- **不引入数据库(指外部 DB)**:用 stdlib SQLite 足够;不引入 Postgres/MySQL 等。
- **生产在 Windows 直跑**:开发期我们用过 WSL→Windows 的 `netsh portproxy`(9333→9222)连 CDP;但 GUI 与 Chrome 都在 Windows 时,直接连 `127.0.0.1:9222`,无需 portproxy。
- **PyInstaller 使用 onedir 免安装包**:T-524 先做 `dist/cmshopee/cmshopee.exe` + `_internal/` 的文件夹分发,不做自动更新器;打包版启动时工作目录切到 exe 所在目录,用户本地配置、DB、图片、日志、登录态和提示词保留在程序目录,更新时只覆盖程序文件。
## 三、构建与运行命令
@@ -51,6 +53,8 @@
| 启动 GUI | `python main.py` / `python -m app` |
| 单元测试(T-006 后) | `python -m unittest discover -s tests` |
| CI 自动验证 | GitHub Actions `.github/workflows/tests.yml`(Windows + Python 3.11 + `QT_QPA_PLATFORM=offscreen`) |
| 安装打包依赖 | `python -m pip install -r requirements-build.txt` |
| 打包 exe | `powershell -ExecutionPolicy Bypass -File scripts\\build_exe.ps1` |
| 跑单账号演示 | `python prototypes/demo.py`(分步)/ `set AUTO=1 && python prototypes/demo.py`(自动) |
| 提交更新(真改线上) | `set UPDATE=1 && python prototypes/demo.py` |
| 读取某账号 Cookie(调试) | `python prototypes/cookies.py` |
@@ -71,6 +75,7 @@ set AUTO=1 && python prototypes/demo.py
## 四、依赖纪律
- 运行时第三方依赖统一写入根目录 `requirements.txt` 并锁定版本;换机或 CI 使用 `python -m pip install -r requirements.txt` 安装。
- 打包依赖只写入 `requirements-build.txt`;PyInstaller 不属于运行时依赖,不写进 `requirements.txt`。
- 当前直接依赖锁定:PySide6 6.5.3、openpyxl 3.1.3、requests 2.31.0、websocket-client 1.6.1、Pillow 9.5.0。
- 新增第三方依赖前,先在本文说明用途、替代方案和维护成本,并同步更新 `requirements.txt`。
- GUI 框架固定为 PySide6,不允许混入 Tkinter/PyQt/Web 形成两套 UI。
+1 -1
View File
@@ -116,7 +116,7 @@
| T-522 | CI:自动跑语法 + 单元/ GUI 测试 | T-006, T-521 | 依据 `docs/engineering-review.md` P1。加 GitHub Actions,在 push/PR 上按 `requirements.txt` 安装依赖并跑 `python -m compileall app main.py` + `python -m unittest discover -s tests`(含 PySide6 环境下的 GUI 测试,`QT_QPA_PLATFORM=offscreen`);不连真实 Shopee/AI;失败即红灯,把"改完必跑测试"变强制门禁 | DONE |
| T-523 | 拆分 `app/gui.py` 为 `app/gui/` 包 | T-511, T-514, T-515, T-516, T-517 | 依据 `docs/engineering-review.md` P0。把 6317 行 God-file 拆为包:`models.py`(3 个 TableModel)、`tabs/`(①~⑤各一文件)、`workers.py`(Generate/Apply/Collect/WriteBack/AccountLoginCheck/AIModelTest 从 gui 挪出,与 `app/workers.py` 基类归拢)、`widgets.py`(色板常量、空状态卡、批次总览等 helper)、`main_window.py`。纯结构重构、对外行为与公开符号不变(保留 `from app import gui` 及 `MainWindow`/各 Tab/Worker 的导入路径或提供兼容再导出),保留 PySide6 缺失优雅降级;68 个 GUI 测试全绿、不删减断言 | DONE |
| T-523a | ②/③ 新一轮运行前清空界面日志显示 | T-519, T-505, T-523 | 问题:②AI生成和③更新shopee进入模块或启动新一轮任务时仍显示上一次运行日志,用户容易把旧失败、旧进度误认为本轮状态。方案:点击②「开始生成」、③「检查本轮更新」或③「开始更新」时,先清空对应界面的可见日志显示区,并写入本轮开始摘要;运行中只追加本轮日志,完成后保留本轮日志。只清空界面文本,不删除 SQLite `run_logs/run_log_events` 或本地 `logs/` 诊断日志;进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志查看作为后续低优先级入口,不自动混入当前运行界面。同步 GUI 测试覆盖新一轮运行不会混入旧日志 | DONE |
| T-524 | PyInstaller 打包为免安装 exe | T-521 | 依据 `docs/engineering-review.md` P1。新增 PyInstaller spec/脚本,产出 Windows 免安装 `.exe`;打包排除并绝不内置 `config.json`/`config/ai_models.json`/`cmshopee.db*`/`chrome_user_data_dir/`/`images/`/`logs/` 等本地数据与密钥;首次运行按现有默认值在本地生成配置;文档补打包与分发步骤 | TODO |
| T-524 | PyInstaller 打包为免安装 exe | T-521 | 依据 `docs/engineering-review.md` P1。新增 PyInstaller spec/脚本,产出 Windows onedir 免安装 `.exe`;打包排除并绝不内置 `config.json`/`config/ai_models.json`/`cmshopee.db*`/`chrome_user_data_dir/`/`images/`/`logs/`/`prompts/`/`title_prompt.txt` 等本地数据与密钥;打包版工作目录切到 exe 所在目录,首次运行按现有默认值在本地生成配置;`scripts/build_exe.ps1` 可构建并校验发布目录,`docs/packaging.md` 说明分发和用户手动覆盖更新步骤 | DONE |
| T-525 | 引入 ruff(lint + format)+ 可选 pre-commit | T-006 | 依据 `docs/engineering-review.md` P1。加 `ruff` 配置(lint + format),先以现状为基线不做大规模风格重排,只开启安全规则(未用 import/变量、明显错误);可选 `.pre-commit-config.yaml`;不改业务逻辑;CI(T-522)可串入 ruff 检查。数据模型渐进上 mypy 作为后续可选 | TODO |
## 里程碑
+4 -1
View File
@@ -25,6 +25,7 @@ cmshopee 是一个给**电商运营**使用的 Windows PySide6 桌面自动化
- [产品与 UI 评估](ux-review.md):以 PM + UI 设计视角评估 5 Tab 模块 / 组件合理性,含优化方案与优先级清单。
- [界面配色设计](ui-color-design.md):语义色板与组件配色映射规范,指导给状态 / 按钮 / 校验 / 登录状态上色。
- [工程评估](engineering-review.md):全栈视角评估工程基础设施与可维护性(依赖清单 / CI / 打包 / gui.py 拆分 / lint),含 P0-P2 与优先级。
- [打包与分发](packaging.md):PyInstaller 免安装 exe 打包命令、排除本地数据规则和用户手动更新方式。
## 运行环境安装
@@ -32,9 +33,11 @@ cmshopee 是一个给**电商运营**使用的 Windows PySide6 桌面自动化
python -m pip install -r requirements.txt
python -m compileall app main.py
python -m unittest discover -s tests
python -m pip install -r requirements-build.txt
powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1
```
`requirements.txt` 只锁定运行所需第三方依赖;`config.json`、`config/ai_models.json`、SQLite、图片、登录态和运营 Excel 仍是本地数据,不提交版本库。
`requirements.txt` 只锁定运行所需第三方依赖;打包环境使用 `requirements-build.txt` 安装 PyInstaller;`config.json`、`config/ai_models.json`、SQLite、图片、登录态和运营 Excel 仍是本地数据,不提交版本库。
GitHub Actions 工作流见 `.github/workflows/tests.yml`,在 push / pull_request 上使用 Windows + Python 3.11 安装 `requirements.txt`,并自动运行语法检查和全量单元/GUI 测试。
+17 -9
View File
File diff suppressed because one or more lines are too long
+100
View File
@@ -0,0 +1,100 @@
# 打包与分发
> T-524 目标:先把当前 Windows 桌面工具打成免安装 onedir `.exe`,方便第一版交付使用。暂不做自动更新器、安装器、增量补丁或在线升级。
## 一、打包前提
- 在 Windows 环境执行。
- Python 环境已能运行源码版 `python main.py`。
- 打包脚本使用当前 PATH 中的 `python`;正式出包前先确认 `python --version` 符合项目目标 Python 3.10+。
- 安装运行依赖和打包依赖:
```powershell
python -m pip install -r requirements-build.txt
```
`requirements-build.txt` 会先安装 `requirements.txt` 的运行依赖,再安装 PyInstaller。
## 二、打包命令
```powershell
powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1
```
脚本会执行:
```powershell
python -m PyInstaller --noconfirm --clean cmshopee.spec
```
成功后输出:
```text
dist\cmshopee\cmshopee.exe
```
发布给用户时,以整个 `dist\cmshopee\` 文件夹为单位压缩分发,用户双击 `cmshopee.exe` 启动。
## 三、绝不打包的本地数据
发布包里不能包含以下本地数据、密钥、业务数据或登录态:
- `config.json`
- `config/ai_models.json`
- `cmshopee.db`、`cmshopee.db-wal`、`cmshopee.db-shm`
- `db.sqlite`
- `chrome_user_data_dir/`
- `images/`
- `logs/`
- `prompts/`
- `title_prompt.txt`
- 运营填写后的 Excel 文件
`cmshopee.spec` 不声明任何 `datas`;`scripts/build_exe.ps1` 会在打包后检查 `dist\cmshopee\`,如果发现上述路径会直接失败。
## 四、首次运行与本地数据位置
打包版启动时,`main.py` 会把工作目录切到 `cmshopee.exe` 所在目录。
因此首次运行时,现有代码会按默认值在 exe 同级目录生成或使用本地文件:
- `config.json`
- `config/ai_models.json`
- `cmshopee.db`
- `chrome_user_data_dir/`
- `images/`
- `logs/`
- `prompts/`、`title_prompt.txt`(用户维护提示词时生成)
这些文件属于用户本地数据,不随新版本程序包覆盖。
## 五、用户后续更新方式
第一版不做自动更新。给用户发新版本时:
1. 让用户先关闭 cmshopee。
2. 建议用户备份当前整个程序文件夹。
3. 解压新版 `dist\cmshopee\`。
4. 只覆盖程序文件,例如 `cmshopee.exe` 和 `_internal/`。
5. 不删除也不覆盖用户原有的 `config.json`、`config/ai_models.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/`、`logs/`、`prompts/`、`title_prompt.txt`。
如果用户把整个旧目录删除再放新版,账号、任务记录、图片和登录态也会一起丢失,只能从备份恢复。
## 六、验证清单
打包前后至少执行:
```powershell
python -m compileall app main.py
python -m unittest discover -s tests
powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1
```
打包成功后确认:
- `dist\cmshopee\cmshopee.exe` 存在。
- `dist\cmshopee\` 中没有第三节列出的本地数据。
- 在干净目录首次启动时能生成默认配置并进入 GUI。
涉及 Shopee/CDP 的真实更新能力,仍按任务文档要求用测试商品做人工回归;打包任务本身不新增自动绕过登录、验证码或风控的能力。
+9 -1
View File
@@ -5,9 +5,17 @@ import os
import sys
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
def _project_root():
if getattr(sys, "frozen", False):
return os.path.dirname(os.path.abspath(sys.executable))
return os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = _project_root()
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
if getattr(sys, "frozen", False):
os.chdir(PROJECT_ROOT)
from app.gui import main
+8
View File
@@ -1087,3 +1087,11 @@
- 边界:不自动登录、不填密码、不绕过验证码;未改商品页、上传、提交逻辑。
- 测试:`tests/test_editor_login.py` 增加真实 accounts 登录 URL 覆盖。
- 验证:`python -m unittest discover -s tests -p "test_editor_login.py"` 通过(37 tests)。
## 【2026-07-03】T-524 完成 · PyInstaller 打包为免安装 exe
- 代码/配置:新增 `cmshopee.spec`、`scripts/build_exe.ps1`、`requirements-build.txt`;`main.py` 在 PyInstaller frozen 模式下把工作目录切到 `cmshopee.exe` 所在目录,确保配置、DB、图片、日志、登录态和提示词在 exe 同级目录生成/读取;`.gitignore` 新增 `build/`、`dist/`、`db.sqlite`、`prompts/`、`title_prompt.txt`。
- 打包安全:`cmshopee.spec` 不声明本地数据 `datas`;构建脚本打包后检查 `dist\cmshopee`,发现 `config.json`、`config/ai_models.json`、`cmshopee.db*`、`db.sqlite`、`chrome_user_data_dir/`、`images/`、`logs/`、`prompts/`、`title_prompt.txt` 会直接失败;脚本会打印当前 Python 版本,低于项目目标 3.10 时给出警告。
- 文档:新增 `docs/packaging.md`,同步 `docs/README.md`、`docs/03-tech-stack.md`、`docs/06-tasks.md`、`docs/current-state.md`;T-524 标为 DONE,下一个可领取任务更新为 T-525。
- 测试:新增 `tests/test_packaging.py` 覆盖 frozen 入口路径、spec 不打包本地数据、构建脚本排除列表。
- 验证:`python -m compileall app main.py` 通过;`python -m unittest discover -s tests -p "test_packaging.py"` 通过(3 tests);`python -m unittest discover -s tests` 通过(170 tests);`git diff --check` 无空白错误(仅 LF/CRLF 提示);`powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1` 成功产出 `dist\cmshopee\cmshopee.exe`,脚本确认未混入本地数据。本机当前 `python` 为 3.7.9,构建脚本已提示低于项目目标;正式发布建议切到 Python 3.10+ 环境后重新打包。
+2
View File
@@ -0,0 +1,2 @@
-r requirements.txt
pyinstaller==5.13.2
+54
View File
@@ -0,0 +1,54 @@
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Resolve-Path (Join-Path $scriptDir "..")
Set-Location $repoRoot
$pythonVersion = python -c "import sys; print('%d.%d.%d' % sys.version_info[:3])"
Write-Host "Using Python: $pythonVersion"
if ([version]$pythonVersion -lt [version]"3.10.0") {
Write-Warning "Project target is Python 3.10+. Switch to a 3.10+ environment before official release builds."
}
$spec = Join-Path $repoRoot "cmshopee.spec"
if (-not (Test-Path -LiteralPath $spec)) {
throw "Missing cmshopee.spec"
}
python -m PyInstaller --noconfirm --clean $spec
$distDir = Join-Path $repoRoot "dist\cmshopee"
$exePath = Join-Path $distDir "cmshopee.exe"
if (-not (Test-Path -LiteralPath $exePath)) {
throw "Build failed: $exePath not found"
}
$excluded = @(
"config.json",
"config\ai_models.json",
"cmshopee.db",
"cmshopee.db-wal",
"cmshopee.db-shm",
"db.sqlite",
"chrome_user_data_dir",
"images",
"logs",
"prompts",
"title_prompt.txt"
)
$found = @()
foreach ($relative in $excluded) {
$candidate = Join-Path $distDir $relative
if (Test-Path -LiteralPath $candidate) {
$found += $relative
}
}
if ($found.Count -gt 0) {
throw "Build output contains local user data: $($found -join ', ')"
}
Write-Host "Build complete: $exePath"
Write-Host "Do not copy local data into the release package."
+48
View File
@@ -0,0 +1,48 @@
import unittest
from _helpers import REPO_ROOT
class PackagingTests(unittest.TestCase):
def read_text(self, relative_path):
return (REPO_ROOT / relative_path).read_text(encoding="utf-8")
def test_frozen_entrypoint_uses_exe_directory(self):
main_py = self.read_text("main.py")
self.assertIn('getattr(sys, "frozen", False)', main_py)
self.assertIn("sys.executable", main_py)
self.assertIn("os.chdir(PROJECT_ROOT)", main_py)
def test_pyinstaller_spec_uses_onedir_without_local_datas(self):
spec = self.read_text("cmshopee.spec")
normalized = "".join(spec.split())
self.assertIn("datas=[]", normalized)
self.assertIn("COLLECT(", spec)
self.assertIn('name="cmshopee"', normalized)
def test_build_script_blocks_user_data_in_release_output(self):
script = self.read_text("scripts/build_exe.ps1")
for token in (
"config.json",
"config\\ai_models.json",
"cmshopee.db",
"cmshopee.db-wal",
"cmshopee.db-shm",
"db.sqlite",
"chrome_user_data_dir",
"images",
"logs",
"prompts",
"title_prompt.txt",
):
self.assertIn(token, script)
self.assertIn("python -m PyInstaller --noconfirm --clean", script)
self.assertIn("cmshopee.spec", script)
if __name__ == "__main__":
unittest.main()