fix(packaging): verify bundled startup dependencies
This commit is contained in:
@@ -7,11 +7,21 @@ include config files, SQLite databases, Chrome profiles, generated images,
|
||||
logs, prompts, or other operator data.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
from PyInstaller.utils.hooks import collect_data_files
|
||||
|
||||
block_cipher = None
|
||||
try:
|
||||
backports_tarfile_spec = importlib.util.find_spec("backports.tarfile")
|
||||
except ModuleNotFoundError:
|
||||
backports_tarfile_spec = None
|
||||
if backports_tarfile_spec is None:
|
||||
raise SystemExit(
|
||||
"Missing backports.tarfile. Run: "
|
||||
"py -3.10 -m pip install -r requirements-build.txt"
|
||||
)
|
||||
default_prompt_datas = collect_data_files(
|
||||
"app.default_prompts",
|
||||
includes=["**/*.txt"],
|
||||
@@ -36,6 +46,7 @@ a = Analysis(
|
||||
"PySide6.QtCore",
|
||||
"PySide6.QtGui",
|
||||
"PySide6.QtWidgets",
|
||||
"backports.tarfile",
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@
|
||||
py -3.10 -m pip install -r requirements-build.txt
|
||||
```
|
||||
|
||||
`requirements-build.txt` 会先安装 `requirements.txt` 的运行依赖,再安装 PyInstaller。依赖必须安装到 Python 3.10 环境里,避免 PATH 上的 Python 3.7/3.12/3.14 打出不可比对的包。运行依赖固定使用 NumPy 1.26.4,以兼容 PySide6/Shiboken 6.5.3;正式打包前不得单独升级到 NumPy 2.x。
|
||||
`requirements-build.txt` 会先安装 `requirements.txt` 的运行依赖,再精确安装 PyInstaller 6.11.1、pyinstaller-hooks-contrib 2026.6、setuptools 80.9.0 和 backports.tarfile 1.2.0。依赖必须安装到 Python 3.10 环境里,避免 PATH 上的 Python 3.7/3.12/3.14 或未锁定的全局构建包装出不可比对的包。运行依赖固定使用 NumPy 1.26.4,以兼容 PySide6/Shiboken 6.5.3;正式打包前不得单独升级到 NumPy 2.x。
|
||||
|
||||
## 二、打包命令
|
||||
|
||||
@@ -38,7 +38,7 @@ dist\cmshopee\
|
||||
|
||||
正式分发入口是 `release\蝦皮圈優化助手<APP_VERSION>.zip`。`dist\cmshopee\` 只是 PyInstaller 构建中间产物,不直接作为正式发包名称。
|
||||
|
||||
当前 `requirements-build.txt` 锁定 PyInstaller 6.11.1,onedir 产物必须是 `cmshopee.exe` + `_internal\` 集中依赖布局。`scripts\build_exe.ps1` 会校验 `_internal\` 存在;缺失时直接失败,避免把不一致的打包环境误发给用户。
|
||||
当前 `requirements-build.txt` 锁定完整构建工具链,`cmshopee.spec` 还会显式检查并收集 `backports.tarfile`,避免 setuptools 的 `pkg_resources/jaraco` 运行时钩子因 vendored 依赖漏收集而在应用入口前崩溃。onedir 产物必须是 `cmshopee.exe` + `_internal\` 集中依赖布局。`scripts\build_exe.ps1` 会校验 `_internal\` 存在,并在组装 release 和压缩 ZIP 前运行 `cmshopee.exe --startup-smoke-test`;自检会完成 PyInstaller 运行时钩子与 GUI 模块导入后立即退出,不创建 GUI、不准备 `data\`、不检查更新或会员状态。自检非零退出或 30 秒超时会直接判定构建失败。
|
||||
|
||||
发布包只包含程序文件和依赖文件:
|
||||
|
||||
@@ -107,7 +107,7 @@ release\
|
||||
|
||||
1. 修改 `app/version.py` 中的 `APP_VERSION`。
|
||||
2. 运行语法检查和单元测试。
|
||||
3. 运行 `powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1`。
|
||||
3. 运行 `powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1`;确认日志包含 `Verified packaged startup smoke test`。
|
||||
4. 检查 `version.txt`、GUI 标题栏、`package-manifest.json`、压缩包文件名和 `release-metadata<APP_VERSION>.json` 的版本一致。
|
||||
5. 在无 Python 环境的 Windows 10/11 机器上解压 `蝦皮圈優化助手<APP_VERSION>.zip` 并启动验证。
|
||||
|
||||
|
||||
+16
-9
@@ -3,7 +3,7 @@ id: T-703
|
||||
title: 修复打包版缺少 backports 并增加 EXE 启动自检
|
||||
phase: 8
|
||||
deps: [T-625]
|
||||
status: TODO
|
||||
status: DONE
|
||||
created: 2026-07-27
|
||||
---
|
||||
|
||||
@@ -61,14 +61,14 @@ ModuleNotFoundError: No module named 'backports'
|
||||
|
||||
## 验收要点
|
||||
|
||||
- [ ] 构建环境安装 `requirements-build.txt` 后可导入 `backports.tarfile`。
|
||||
- [ ] `cmshopee.spec` 显式包含 `backports.tarfile`,缺少依赖时构建立即失败。
|
||||
- [ ] `py -3.10 main.py --startup-smoke-test` 正常退出,不创建 GUI 或业务数据。
|
||||
- [ ] 构建脚本在压缩发布包前运行打包 EXE 启动自检,并处理非零退出和 30 秒超时。
|
||||
- [ ] 新打包的 `cmshopee.exe --startup-smoke-test` 返回 0,不再出现 `No module named 'backports'`。
|
||||
- [ ] 发布目录和 ZIP 不包含 `data/`、配置、数据库、图片、日志或登录态。
|
||||
- [ ] 本轮发布包关闭并隐藏会员订阅检测,但源码在打包后恢复当前开发状态。
|
||||
- [ ] 验证通过:
|
||||
- [x] 构建环境安装 `requirements-build.txt` 后可导入 `backports.tarfile`。
|
||||
- [x] `cmshopee.spec` 显式包含 `backports.tarfile`,缺少依赖时构建立即失败。
|
||||
- [x] `py -3.10 main.py --startup-smoke-test` 正常退出,不创建 GUI 或业务数据。
|
||||
- [x] 构建脚本在压缩发布包前运行打包 EXE 启动自检,并处理非零退出和 30 秒超时。
|
||||
- [x] 新打包的 `cmshopee.exe --startup-smoke-test` 返回 0,不再出现 `No module named 'backports'`。
|
||||
- [x] 发布目录和 ZIP 不包含 `data/`、配置、数据库、图片、日志或登录态。
|
||||
- [x] 本轮发布包关闭并隐藏会员订阅检测,但源码在打包后恢复当前开发状态。
|
||||
- [x] 验证通过:
|
||||
- `py -3.10 -m unittest discover -s tests -p test_packaging.py`
|
||||
- `py -3.10 -m unittest discover -s tests`
|
||||
- `py -3.10 -m ruff check app tests main.py`
|
||||
@@ -86,3 +86,10 @@ ModuleNotFoundError: No module named 'backports'
|
||||
## 执行记录
|
||||
|
||||
- 2026-07-27:根据 `0.1.5.2` 打包版真实启动异常创建任务。确认异常发生在 PyInstaller `pyi_rth_pkgres` 运行时钩子,早于 `main.py`;构建警告和发布目录共同证明 `setuptools` vendored `jaraco.context` 已收集、`backports` 未收集,与会员订阅临时开关无关。
|
||||
- 2026-07-27:`requirements-build.txt` 精确锁定 PyInstaller 6.11.1、pyinstaller-hooks-contrib 2026.6、setuptools 80.9.0 和 backports.tarfile 1.2.0;安装后确认 Python 3.10 可从独立分发目录导入 `backports.tarfile`。
|
||||
- 2026-07-27:`cmshopee.spec` 在分析前检查 `backports.tarfile`,缺失时给出构建依赖安装命令并中止;主程序 hidden import 显式包含该模块。真实构建日志已出现 `Analyzing hidden import 'backports.tarfile'`。
|
||||
- 2026-07-27:`main.py` 增加内部参数 `--startup-smoke-test`,在 `app.gui` 成功导入后直接返回 0,不创建 GUI 或进入数据、更新、会员、Chrome 和业务流程。构建脚本在 release 组装与 ZIP 压缩前以隐藏窗口运行打包 EXE,非零退出或 30 秒超时会失败并终止发布。
|
||||
- 2026-07-27:扩展打包回归测试,覆盖构建依赖版本、spec 依赖检查、源码启动自检和 PowerShell 自检顺序;既有版本格式断言同步兼容当前三段或四段纯数字版本,不修改用户当前 `app/version.py`。
|
||||
- 2026-07-27:真实运行 `scripts/build_exe.ps1` 成功,dist 和 release 两处 `cmshopee.exe --startup-smoke-test` 均返回 0,未再出现 `No module named 'backports'`。构建警告只剩可选的 `backports.zstd`,不影响当前依赖或启动。
|
||||
- 2026-07-27:按发布要求在构建期间临时关闭并隐藏会员订阅检测;生成 `release/蝦皮圈優化助手0.1.5.2.zip` 后已精确恢复源码开关,`app/gui/main_window.py` 无未提交差异。新 ZIP 大小 71,420,205 字节,SHA-256 为 `d1929859b178ac409725c885a936b96109afc1840bc6dd2b74064089e158506f`。
|
||||
- 2026-07-27:验证通过:`py -3.10 -m unittest discover -s tests -p test_packaging.py`(12 项)、`py -3.10 -m unittest discover -s tests`(702 项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1`、release EXE 启动自检和 `git diff --check`。
|
||||
|
||||
@@ -18,5 +18,15 @@ if PROJECT_ROOT not in sys.path:
|
||||
from app.gui import main
|
||||
|
||||
|
||||
STARTUP_SMOKE_TEST_ARG = "--startup-smoke-test"
|
||||
|
||||
|
||||
def _entrypoint(argv=None):
|
||||
args = sys.argv[1:] if argv is None else list(argv)
|
||||
if STARTUP_SMOKE_TEST_ARG in args:
|
||||
return 0
|
||||
return main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
raise SystemExit(_entrypoint())
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
-r requirements.txt
|
||||
pyinstaller==6.11.1
|
||||
pyinstaller-hooks-contrib==2026.6
|
||||
setuptools==80.9.0
|
||||
backports.tarfile==1.2.0
|
||||
|
||||
@@ -61,6 +61,29 @@ function Assert-NoExcludedData {
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-PackagedStartup {
|
||||
param(
|
||||
[string]$ExePath,
|
||||
[int]$TimeoutSeconds = 30
|
||||
)
|
||||
|
||||
$process = Start-Process `
|
||||
-FilePath $ExePath `
|
||||
-ArgumentList @("--startup-smoke-test") `
|
||||
-PassThru `
|
||||
-WindowStyle Hidden
|
||||
if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
|
||||
if (-not $process.HasExited) {
|
||||
Stop-Process -Id $process.Id -Force
|
||||
}
|
||||
throw "打包程序启动自检超时(${TimeoutSeconds}秒): $ExePath"
|
||||
}
|
||||
$process.Refresh()
|
||||
if ($process.ExitCode -ne 0) {
|
||||
throw "打包程序启动自检失败,退出码: $($process.ExitCode)"
|
||||
}
|
||||
}
|
||||
|
||||
function Compress-ArchiveWithRetry {
|
||||
param(
|
||||
[string]$SourcePath,
|
||||
@@ -160,6 +183,8 @@ if (-not (Test-Path -LiteralPath $internalDir)) {
|
||||
throw "Build output missing _internal directory. Expected PyInstaller 6.x onedir layout (cmshopee.exe + _internal). Ensure PyInstaller >= 6.0 is installed: py -3.10 -m pip install -r requirements-build.txt"
|
||||
}
|
||||
Write-Host "Verified onedir layout: _internal present"
|
||||
Assert-PackagedStartup -ExePath $exePath -TimeoutSeconds 30
|
||||
Write-Host "Verified packaged startup smoke test"
|
||||
|
||||
$releaseRoot = Join-Path $repoRoot "release"
|
||||
$releaseName = "$appName$appVersion"
|
||||
|
||||
+37
-1
@@ -1,5 +1,7 @@
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -30,6 +32,9 @@ class PackagingTests(unittest.TestCase):
|
||||
self.assertIn('"app.default_prompts"', spec)
|
||||
self.assertIn('includes=["**/*.txt"]', normalized)
|
||||
self.assertIn("datas=default_prompt_datas", normalized)
|
||||
self.assertIn('find_spec("backports.tarfile")', spec)
|
||||
self.assertIn('"backports.tarfile"', spec)
|
||||
self.assertIn("requirements-build.txt", spec)
|
||||
self.assertIn("COLLECT(", spec)
|
||||
self.assertIn('name="cmshopee"', normalized)
|
||||
cover_prompt = self.read_text("app/default_prompts/cover/cover_prompt.txt")
|
||||
@@ -66,12 +71,35 @@ class PackagingTests(unittest.TestCase):
|
||||
self.assertIn("cmshopee.spec", script)
|
||||
self.assertIn("Decode-Utf8Base64", script)
|
||||
|
||||
def test_build_requirements_pin_pyinstaller_runtime_dependencies(self):
|
||||
requirements = self.read_text("requirements-build.txt")
|
||||
|
||||
for dependency in (
|
||||
"pyinstaller==6.11.1",
|
||||
"pyinstaller-hooks-contrib==2026.6",
|
||||
"setuptools==80.9.0",
|
||||
"backports.tarfile==1.2.0",
|
||||
):
|
||||
self.assertIn(dependency, requirements)
|
||||
|
||||
def test_source_startup_smoke_test_imports_gui_and_exits(self):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(REPO_ROOT / "main.py"), "--startup-smoke-test"],
|
||||
cwd=REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
|
||||
def test_release_version_source_is_single_place(self):
|
||||
version_py = self.read_text("app/version.py")
|
||||
|
||||
self.assertIn('APP_NAME = "蝦皮圈優化助手"', version_py)
|
||||
self.assertIn('APP_CODE_NAME = "cmshopee"', version_py)
|
||||
self.assertRegex(version.APP_VERSION, r"^\d+\.\d+\.\d+$")
|
||||
self.assertRegex(version.APP_VERSION, r"^\d+(?:\.\d+){2,3}$")
|
||||
self.assertEqual(
|
||||
f"{version.APP_NAME} v{version.APP_VERSION}",
|
||||
version.display_name(),
|
||||
@@ -122,6 +150,14 @@ class PackagingTests(unittest.TestCase):
|
||||
self.assertNotIn('release-metadata*.json', script)
|
||||
self.assertIn('cmshopee-updater.spec', script)
|
||||
self.assertIn('cmshopee-updater.exe', script)
|
||||
self.assertIn("Assert-PackagedStartup", script)
|
||||
self.assertIn("--startup-smoke-test", script)
|
||||
self.assertIn("WaitForExit", script)
|
||||
self.assertIn("Stop-Process", script)
|
||||
self.assertLess(
|
||||
script.index("Assert-PackagedStartup -ExePath"),
|
||||
script.index('$releaseRoot = Join-Path $repoRoot "release"'),
|
||||
)
|
||||
|
||||
def test_updater_spec_builds_a_windowed_standalone_executable(self):
|
||||
spec = self.read_text("cmshopee-updater.spec")
|
||||
|
||||
Reference in New Issue
Block a user