mirror of
https://github.com/zhouxiaoka/autoclip.git
synced 2026-09-02 22:15:18 +08:00
chore: remove dead PyInstaller-route scripts; rewrite build docs
After the CI unification, the PyInstaller / prepare_resources route scripts were fully orphaned (referenced only by each other and stale docs). Remove the cluster: build_backend.py, prepare_resources.py, check_bundle_size.py, smoke_api.py, build_desktop.py, dev_desktop.py, test_desktop.py, quick_build_check.sh, build_release.py scripts/ now holds only what's live: build_macos_arm.sh (the one packaging route), verify_desktop.sh (backend smoke), monitor_whisper.py (runtime). Rewrite scripts/README.md and BUILD_GUIDE.md to describe the single PBS route accurately (they previously documented the dead PyInstaller flow), and fix the stale build command in RELEASE_CHECKLIST.md. Also removed a 94M stale _autoclip-backend-pyinstaller-bak left in src-tauri/resources/ (untracked build junk). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
224
BUILD_GUIDE.md
224
BUILD_GUIDE.md
@@ -1,205 +1,71 @@
|
||||
# 🚀 AutoClip Desktop 构建指南
|
||||
|
||||
## 📋 概述
|
||||
桌面客户端只有**一条**打包路线:python-build-standalone(PBS)。它把便携 Python 运行时、
|
||||
后端源码、静态 ffmpeg/ffprobe 全部打进 `.app`,用户机器**无需预装 Python 或 ffmpeg**。
|
||||
|
||||
AutoClip Desktop支持跨平台构建,包括macOS (Intel/ARM)、Windows和Linux。由于架构限制,我们采用以下策略:
|
||||
> 历史上的 PyInstaller / prepare_resources 路线(以及对应的 6+ 个 CI 工作流)从未成功产出可用包,已全部移除。
|
||||
|
||||
- **M芯片Mac**: 本地构建M芯片版本
|
||||
- **其他平台**: 使用GitHub Actions CI/CD构建
|
||||
## 本地构建(macOS Apple Silicon)
|
||||
|
||||
## 🛠️ 本地构建 (M芯片Mac)
|
||||
|
||||
### 快速构建
|
||||
```bash
|
||||
# 使用专用脚本
|
||||
./scripts/build_macos_arm.sh
|
||||
```
|
||||
|
||||
### 手动构建
|
||||
```bash
|
||||
# 1. 激活虚拟环境
|
||||
source venv/bin/activate
|
||||
|
||||
# 2. 构建前端
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# 3. 构建后端
|
||||
export AUTOCLIP_DESKTOP_MODE=1
|
||||
export AUTOCLIP_MODE=desktop
|
||||
python -m PyInstaller --onefile --name autoclip-backend \
|
||||
--distpath src-tauri/resources \
|
||||
--workpath build/pyinstaller \
|
||||
--specpath build/specs \
|
||||
--clean --noconfirm \
|
||||
backend/desktop_main.py
|
||||
|
||||
# 4. 准备FFmpeg
|
||||
mkdir -p src-tauri/resources/ffmpeg-unified
|
||||
cp $(which ffmpeg) src-tauri/resources/ffmpeg-unified/ffmpeg-macos
|
||||
chmod +x src-tauri/resources/ffmpeg-unified/ffmpeg-macos
|
||||
|
||||
# 5. 构建Tauri应用
|
||||
cd src-tauri
|
||||
tauri build
|
||||
cd ..
|
||||
产物:
|
||||
```
|
||||
src-tauri/target/release/bundle/macos/
|
||||
├── AutoClip Desktop.app # 应用包(~550M)
|
||||
└── AutoClip Desktop_1.0.0_aarch64.dmg # DMG 安装包(~260M)
|
||||
```
|
||||
|
||||
## 🌐 跨平台构建 (GitHub Actions)
|
||||
脚本各步骤说明见 [`scripts/README.md`](scripts/README.md)。
|
||||
|
||||
### 前置依赖
|
||||
|
||||
| 工具 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| Node.js | 18+ | 前端构建 |
|
||||
| Rust | stable | 带 `aarch64-apple-darwin` target |
|
||||
| cargo-tauri | 2.x | `cargo install tauri-cli` |
|
||||
|
||||
系统 **不需要** 预装 Python / ffmpeg —— 脚本自带便携版(首次构建会下载并缓存到 `build/`)。
|
||||
|
||||
## CI / 发布(GitHub Actions)
|
||||
|
||||
`.github/workflows/desktop-build.yml` 跑同一个 `build_macos_arm.sh`:
|
||||
|
||||
### 触发构建
|
||||
```bash
|
||||
# 创建标签触发构建
|
||||
# 手动触发:仓库页面 → Actions → "Desktop Build (macOS arm64)" → Run workflow
|
||||
|
||||
# 或打 tag 触发,并自动把 DMG 挂到 GitHub Release:
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
|
||||
# 或手动触发
|
||||
# 在GitHub仓库页面 -> Actions -> 跨平台构建 -> Run workflow
|
||||
```
|
||||
|
||||
### 构建平台
|
||||
- **macOS Intel**: x86_64-apple-darwin
|
||||
- **Windows**: x86_64-pc-windows-msvc
|
||||
- **Linux**: x86_64-unknown-linux-gnu
|
||||
## 安装与首次运行
|
||||
|
||||
## 📦 构建产物
|
||||
DMG 是 ad-hoc 签名(未做 Apple 公证),所以:
|
||||
|
||||
### 本地构建 (M芯片Mac)
|
||||
```
|
||||
src-tauri/target/release/bundle/
|
||||
├── macos/
|
||||
│ └── AutoClip Desktop.app # macOS应用包
|
||||
└── dmg/
|
||||
└── AutoClip Desktop_1.0.0_aarch64.dmg # DMG安装包
|
||||
```
|
||||
1. 双击 DMG,把 `AutoClip Desktop` 拖到 Applications
|
||||
2. **首次打开右键点应用 → 选「打开」** 以绕过 Gatekeeper
|
||||
3. 后端会自动启动并在 `~/Library/Application Support/AutoClip` 建数据目录
|
||||
|
||||
### GitHub Actions构建
|
||||
```
|
||||
artifacts/
|
||||
├── autoclip-macos-intel/
|
||||
│ └── AutoClip Desktop_1.0.0_x64.dmg
|
||||
├── autoclip-windows/
|
||||
│ └── AutoClip Desktop_1.0.0_x64.exe
|
||||
└── autoclip-linux/
|
||||
└── AutoClip Desktop_1.0.0_x86_64.AppImage
|
||||
```
|
||||
## 故障排除
|
||||
|
||||
## 🔧 依赖要求
|
||||
|
||||
### 本地构建
|
||||
- **Node.js**: 18+
|
||||
- **Python**: 3.11+
|
||||
- **Rust**: 1.70+
|
||||
- **Tauri CLI**: 2.0+
|
||||
- **FFmpeg**: 系统安装
|
||||
|
||||
### GitHub Actions
|
||||
- 自动安装所有依赖
|
||||
- 支持所有目标平台
|
||||
- 自动生成发布包
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 本地测试 (M芯片Mac)
|
||||
命令行直接跑 app 看后端输出:
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone <repository-url>
|
||||
cd autoclip
|
||||
|
||||
# 安装依赖
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 构建
|
||||
./scripts/build_macos_arm.sh
|
||||
'/Applications/AutoClip Desktop.app/Contents/MacOS/autoclip-desktop'
|
||||
```
|
||||
应看到 `Backend started on port: XXXXX` 和 `Application startup complete`。
|
||||
|
||||
### 2. 跨平台发布
|
||||
```bash
|
||||
# 提交代码
|
||||
git add .
|
||||
git commit -m "Release v1.0.0"
|
||||
git push
|
||||
| 现象 | 排查 |
|
||||
|------|------|
|
||||
| `ModuleNotFoundError: No module named 'X'` | 把 `X` 加进 `requirements.txt` 重新构建(构建期的依赖检查应该已经拦下,正常不会发生)|
|
||||
| 窗口黑屏 | 前端没挂载,看 WebView 控制台;通常是打包/资源问题 |
|
||||
| 视频处理失败 | 确认 `Contents/Resources/resources/ffmpeg/{ffmpeg,ffprobe}` 存在且可执行 |
|
||||
| 构建失败想重来 | `rm -rf src-tauri/target build/pbs-cache build/ffmpeg-cache` 后重跑(会重新下载)|
|
||||
|
||||
# 创建标签
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
## 已知限制
|
||||
|
||||
# GitHub Actions会自动构建所有平台
|
||||
```
|
||||
|
||||
## 📋 构建检查清单
|
||||
|
||||
### 构建前检查
|
||||
- [ ] 代码已提交
|
||||
- [ ] 依赖已安装
|
||||
- [ ] 环境变量已设置
|
||||
- [ ] FFmpeg已安装
|
||||
|
||||
### 构建后验证
|
||||
- [ ] 应用包存在
|
||||
- [ ] 安装包存在
|
||||
- [ ] 文件大小合理
|
||||
- [ ] 可以正常安装
|
||||
- [ ] 应用可以启动
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. PyInstaller未找到
|
||||
```bash
|
||||
# 激活虚拟环境
|
||||
source venv/bin/activate
|
||||
pip install pyinstaller
|
||||
```
|
||||
|
||||
#### 2. Tauri CLI未找到
|
||||
```bash
|
||||
# 安装Tauri CLI
|
||||
cargo install tauri-cli
|
||||
```
|
||||
|
||||
#### 3. FFmpeg未找到
|
||||
```bash
|
||||
# macOS
|
||||
brew install ffmpeg
|
||||
|
||||
# 或使用conda
|
||||
conda install ffmpeg
|
||||
```
|
||||
|
||||
#### 4. 构建失败
|
||||
```bash
|
||||
# 清理构建缓存
|
||||
rm -rf build/
|
||||
rm -rf src-tauri/target/
|
||||
|
||||
# 重新构建
|
||||
./scripts/build_macos_arm.sh
|
||||
```
|
||||
|
||||
### 日志查看
|
||||
```bash
|
||||
# 查看构建日志
|
||||
tail -f logs/backend.log
|
||||
tail -f logs/frontend.log
|
||||
```
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如果遇到构建问题,请:
|
||||
|
||||
1. 查看构建日志
|
||||
2. 检查依赖版本
|
||||
3. 提交Issue到GitHub
|
||||
4. 提供详细的错误信息
|
||||
|
||||
## 🔄 更新说明
|
||||
|
||||
- **v1.0.0**: 初始版本,支持M芯片Mac
|
||||
- **v1.1.0**: 添加跨平台构建支持
|
||||
- **v1.2.0**: 优化构建流程和错误处理
|
||||
- 仅 Apple Silicon (arm64),暂无 Intel / Windows / Linux 包
|
||||
- ad-hoc 签名、未公证,首次需右键打开
|
||||
|
||||
@@ -65,11 +65,11 @@
|
||||
|
||||
### 2. 构建阶段
|
||||
```bash
|
||||
# 运行发布构建脚本
|
||||
python scripts/build_release.py
|
||||
# 打桌面客户端(macOS arm64),详见 BUILD_GUIDE.md
|
||||
./scripts/build_macos_arm.sh
|
||||
|
||||
# 验证构建产物
|
||||
ls -la releases/
|
||||
ls -la "src-tauri/target/release/bundle/macos/"
|
||||
```
|
||||
|
||||
### 3. 测试阶段
|
||||
|
||||
@@ -1,240 +1,63 @@
|
||||
# AutoClip 桌面客户端开发脚本
|
||||
# scripts/
|
||||
|
||||
本目录包含了AutoClip桌面客户端的开发、构建和测试脚本。
|
||||
构建与运维脚本。桌面客户端只有**一条**打包路线(python-build-standalone,下称 PBS),
|
||||
历史上的 PyInstaller / prepare_resources 路线及其脚本已全部移除。
|
||||
|
||||
## 📁 脚本说明
|
||||
## 脚本清单
|
||||
|
||||
### 🔧 构建脚本
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| `build_macos_arm.sh` | **唯一的桌面打包脚本**(macOS Apple Silicon)。端到端产出 `.app` + `.dmg`。 |
|
||||
| `verify_desktop.sh` | 后端冒烟测试:`cargo check` + 起后端,校验 `/health` 与 `/api/v1/video-categories`。被 `nightly-desktop-smoke.yml` 调用。 |
|
||||
| `monitor_whisper.py` | 运行期 Whisper 任务监控,被根目录 `start_autoclip.sh` / `check_whisper_status.sh` 调用。 |
|
||||
|
||||
#### `build_backend.py`
|
||||
构建后端二进制文件,使用PyInstaller打包Python代码。
|
||||
## 打包桌面客户端(macOS arm64)
|
||||
|
||||
```bash
|
||||
python scripts/build_backend.py
|
||||
./scripts/build_macos_arm.sh
|
||||
```
|
||||
|
||||
**功能:**
|
||||
- 安装Python依赖
|
||||
- 安装PyInstaller
|
||||
- 创建PyInstaller规格文件
|
||||
- 构建单文件可执行程序
|
||||
- 将构建结果复制到Tauri资源目录
|
||||
|
||||
#### `build_desktop.py`
|
||||
构建完整的桌面应用,包括前端、后端和Tauri应用。
|
||||
|
||||
```bash
|
||||
python scripts/build_desktop.py
|
||||
产物:
|
||||
```
|
||||
src-tauri/target/release/bundle/macos/
|
||||
├── AutoClip Desktop.app
|
||||
└── AutoClip Desktop_1.0.0_aarch64.dmg
|
||||
```
|
||||
|
||||
**功能:**
|
||||
- 检查构建环境(Node.js、npm、Rust、Tauri CLI)
|
||||
- 构建后端二进制
|
||||
- 安装前端依赖
|
||||
- 构建前端
|
||||
- 构建Tauri应用
|
||||
- 显示构建结果
|
||||
### 这个脚本做了什么
|
||||
|
||||
### 🚀 开发脚本
|
||||
1. 下载便携 Python 运行时(python-build-standalone,缓存在 `build/pbs-cache/`)
|
||||
2. 用便携 Python 安装 `requirements.txt` 的全部依赖
|
||||
3. **依赖完整性检查**:AST 扫描后端所有第三方 import,缺任何一个就让构建失败
|
||||
(防止"开发机能跑、打包就 500")
|
||||
4. rsync 拷贝后端源码到 `src-tauri/resources/backend/`
|
||||
5. 下载**静态** ffmpeg + ffprobe(arm64,自包含,零 homebrew 依赖,缓存在 `build/ffmpeg-cache/`)
|
||||
6. 构建前端(`npm ci && npm run build`)
|
||||
7. `cargo tauri build --bundles app`
|
||||
8. 把 `python/`、`backend/`、`ffmpeg/` 注入 `.app/Contents/Resources/resources/`
|
||||
9. ad-hoc 签名 + `hdiutil` 手动打 DMG
|
||||
|
||||
#### `dev_desktop.py`
|
||||
管理桌面应用的开发环境。
|
||||
### 前置依赖
|
||||
|
||||
```bash
|
||||
# 启动开发环境
|
||||
python scripts/dev_desktop.py start
|
||||
|
||||
# 停止开发环境
|
||||
python scripts/dev_desktop.py stop
|
||||
|
||||
# 重启开发环境
|
||||
python scripts/dev_desktop.py restart
|
||||
|
||||
# 查看状态
|
||||
python scripts/dev_desktop.py status
|
||||
|
||||
# 安装依赖
|
||||
python scripts/dev_desktop.py install
|
||||
```
|
||||
|
||||
**功能:**
|
||||
- 自动安装依赖
|
||||
- 启动后端服务
|
||||
- 启动前端开发服务器
|
||||
- 启动Tauri开发模式
|
||||
- 进程管理和监控
|
||||
- 服务健康检查
|
||||
|
||||
### 🧪 测试脚本
|
||||
|
||||
#### `test_desktop.py`
|
||||
运行桌面应用的功能测试。
|
||||
|
||||
```bash
|
||||
python scripts/test_desktop.py
|
||||
```
|
||||
|
||||
**测试项目:**
|
||||
- 文件结构完整性
|
||||
- Python依赖检查
|
||||
- Node.js依赖检查
|
||||
- Rust环境检查
|
||||
- Tauri配置验证
|
||||
- Cargo配置验证
|
||||
- 前端构建测试
|
||||
- 后端API测试(需要服务运行)
|
||||
|
||||
## 🛠️ 环境要求
|
||||
|
||||
### 必需工具
|
||||
- **Python 3.11+** - 后端开发
|
||||
- **Node.js 18+** - 前端开发
|
||||
- **Rust 1.70+** - Tauri应用开发
|
||||
- **Tauri CLI** - 桌面应用构建
|
||||
|
||||
### 安装Tauri CLI
|
||||
```bash
|
||||
npm install -g @tauri-apps/cli
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 开发环境
|
||||
```bash
|
||||
# 启动开发环境
|
||||
python scripts/dev_desktop.py start
|
||||
|
||||
# 查看状态
|
||||
python scripts/dev_desktop.py status
|
||||
|
||||
# 停止开发环境
|
||||
python scripts/dev_desktop.py stop
|
||||
```
|
||||
|
||||
### 2. 功能测试
|
||||
```bash
|
||||
# 运行所有测试
|
||||
python scripts/test_desktop.py
|
||||
|
||||
# 查看帮助
|
||||
python scripts/test_desktop.py --help
|
||||
```
|
||||
|
||||
### 3. 构建发布版本
|
||||
```bash
|
||||
# 构建完整桌面应用
|
||||
python scripts/build_desktop.py
|
||||
```
|
||||
|
||||
## 📊 项目结构
|
||||
|
||||
```
|
||||
autoclip/
|
||||
├── backend/ # 后端代码
|
||||
│ ├── desktop_main.py # 桌面模式主启动文件
|
||||
│ ├── desktop_celery.py # 桌面模式Celery配置
|
||||
│ ├── core/desktop_config.py # 桌面配置管理
|
||||
│ └── api/v1/desktop.py # 桌面专用API
|
||||
├── src-tauri/ # Tauri桌面应用
|
||||
│ ├── src/
|
||||
│ │ ├── lib.rs # 主入口
|
||||
│ │ ├── backend_manager.rs # 后端管理器
|
||||
│ │ ├── commands.rs # Tauri命令
|
||||
│ │ └── tray.rs # 系统托盘
|
||||
│ ├── tauri.conf.json # Tauri配置
|
||||
│ └── Cargo.toml # Rust依赖
|
||||
├── frontend/src/components/ # 前端组件
|
||||
│ ├── DesktopSettings.tsx # 桌面设置界面
|
||||
│ └── OfflineModeIndicator.tsx # 离线模式指示器
|
||||
└── scripts/ # 开发脚本
|
||||
├── build_backend.py # 后端构建脚本
|
||||
├── build_desktop.py # 完整构建脚本
|
||||
├── dev_desktop.py # 开发环境管理
|
||||
├── test_desktop.py # 功能测试脚本
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 桌面模式配置
|
||||
桌面应用使用专门的配置系统,配置文件位置:
|
||||
- **macOS**: `~/Library/Application Support/AutoClip/config.json`
|
||||
- **Windows**: `%LOCALAPPDATA%/AutoClip/config.json`
|
||||
- Node.js 18+、Rust(带 `aarch64-apple-darwin` target)、cargo-tauri (`cargo install tauri-cli`)
|
||||
- 系统 **不需要** 预装 Python / ffmpeg —— 脚本会自带便携版
|
||||
|
||||
### 环境变量
|
||||
开发模式下会自动设置以下环境变量:
|
||||
- `AUTOCLIP_DESKTOP_MODE=true`
|
||||
- `AUTOCLIP_MODE=desktop`
|
||||
|
||||
## 🐛 故障排除
|
||||
- `PIP_INDEX_URL`:pip 源,默认清华镜像;CI 里设为 `https://pypi.org/simple`
|
||||
|
||||
### 常见问题
|
||||
## CI
|
||||
|
||||
#### 1. 后端服务启动失败
|
||||
`.github/workflows/desktop-build.yml` 在 `workflow_dispatch` 和 `v*` tag 上跑同一个
|
||||
`build_macos_arm.sh`,打 tag 时把 DMG 挂到 GitHub Release。
|
||||
|
||||
> 当前仅 macOS arm64。PBS 与静态 ffmpeg 对 Intel/Windows/Linux 都有对应版本,
|
||||
> 后续把 runner、下载 URL、tauri target 参数化即可泛化。
|
||||
|
||||
## 开发模式(不打包)
|
||||
|
||||
直接用 Tauri 开发模式,热重载:
|
||||
```bash
|
||||
# 检查端口是否被占用
|
||||
lsof -i :8000
|
||||
|
||||
# 检查Python依赖
|
||||
python -c "import fastapi, uvicorn, celery"
|
||||
cd src-tauri && cargo tauri dev
|
||||
```
|
||||
|
||||
#### 2. 前端构建失败
|
||||
```bash
|
||||
# 清理node_modules并重新安装
|
||||
cd frontend
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 3. Tauri构建失败
|
||||
```bash
|
||||
# 检查Rust环境
|
||||
rustc --version
|
||||
cargo --version
|
||||
|
||||
# 检查Tauri CLI
|
||||
tauri --version
|
||||
```
|
||||
|
||||
#### 4. 权限问题
|
||||
```bash
|
||||
# 给脚本执行权限
|
||||
chmod +x scripts/*.py
|
||||
```
|
||||
|
||||
### 日志查看
|
||||
- **后端日志**: `data/logs/autoclip.log`
|
||||
- **开发进程**: `dev_processes.json`
|
||||
|
||||
## 📝 开发说明
|
||||
|
||||
### 代码结构
|
||||
- **后端**: 使用FastAPI + Celery + SQLite
|
||||
- **前端**: 使用React + TypeScript + Ant Design
|
||||
- **桌面**: 使用Tauri + Rust
|
||||
|
||||
### 开发流程
|
||||
1. 使用 `dev_desktop.py start` 启动开发环境
|
||||
2. 修改代码,自动热重载
|
||||
3. 使用 `test_desktop.py` 运行测试
|
||||
4. 使用 `build_desktop.py` 构建发布版本
|
||||
|
||||
### 贡献指南
|
||||
1. Fork项目
|
||||
2. 创建功能分支
|
||||
3. 运行测试确保通过
|
||||
4. 提交Pull Request
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如果遇到问题,请:
|
||||
1. 查看本文档的故障排除部分
|
||||
2. 运行 `test_desktop.py` 检查环境
|
||||
3. 查看日志文件
|
||||
4. 提交Issue到项目仓库
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2024年12月
|
||||
**版本**: 1.0.0
|
||||
(前端 :3000 + 后端动态端口,由 `backend_manager.rs` 拉起)
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
构建后端二进制文件 - 桌面版
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="构建桌面版后端二进制文件")
|
||||
parser.add_argument(
|
||||
"--install-deps",
|
||||
action="store_true",
|
||||
help="构建前安装 requirements.txt 和 PyInstaller。默认只使用现有虚拟环境依赖。",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print("[BUILD] Starting desktop backend build...")
|
||||
|
||||
# 设置路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
backend_dir = project_root / "backend"
|
||||
resources_dir = project_root / "src-tauri" / "resources"
|
||||
|
||||
# 确保资源目录存在
|
||||
resources_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 检查虚拟环境(本地开发)或使用当前 Python(CI)
|
||||
venv_path = project_root / "venv"
|
||||
if venv_path.exists():
|
||||
pip_path = venv_path / "bin" / "pip"
|
||||
python_path = venv_path / "bin" / "python"
|
||||
else:
|
||||
# CI 或无 venv 环境:使用当前 Python
|
||||
pip_path = sys.executable
|
||||
python_path = sys.executable
|
||||
# 将 pip install 改为 python -m pip
|
||||
print("[BUILD] No venv detected, using current Python environment")
|
||||
|
||||
if args.install_deps:
|
||||
# 安装依赖
|
||||
print("📦 安装Python依赖...")
|
||||
try:
|
||||
# 如果 pip_path 就是 sys.executable,用 python -m pip
|
||||
if pip_path == sys.executable:
|
||||
subprocess.run([str(python_path), "-m", "pip", "install", "-r", "requirements.txt"],
|
||||
check=True, cwd=project_root)
|
||||
else:
|
||||
subprocess.run([str(pip_path), "install", "-r", "requirements.txt"],
|
||||
check=True, cwd=project_root)
|
||||
print("✅ Python依赖安装成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Python依赖安装失败: {e}")
|
||||
return 1
|
||||
|
||||
# 安装PyInstaller
|
||||
print("📦 安装PyInstaller...")
|
||||
try:
|
||||
if pip_path == sys.executable:
|
||||
subprocess.run([str(python_path), "-m", "pip", "install", "pyinstaller"], check=True)
|
||||
else:
|
||||
subprocess.run([str(pip_path), "install", "pyinstaller"], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ PyInstaller安装失败: {e}")
|
||||
return 1
|
||||
else:
|
||||
try:
|
||||
subprocess.run(
|
||||
[str(python_path), "-m", "PyInstaller", "--version"],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
print("❌ PyInstaller 不可用,请先运行: python scripts/build_backend.py --install-deps")
|
||||
return 1
|
||||
|
||||
# 创建PyInstaller规格文件
|
||||
spec_content = f"""
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_submodules
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['{backend_dir}/desktop_main.py'],
|
||||
pathex=['{project_root}', '{backend_dir}'],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('{backend_dir}/core', 'backend/core'),
|
||||
('{backend_dir}/api', 'backend/api'),
|
||||
('{backend_dir}/models', 'backend/models'),
|
||||
('{backend_dir}/services', 'backend/services'),
|
||||
('{backend_dir}/repositories', 'backend/repositories'),
|
||||
('{backend_dir}/schemas', 'backend/schemas'),
|
||||
('{backend_dir}/utils', 'backend/utils'),
|
||||
('{backend_dir}/tasks', 'backend/tasks'),
|
||||
('{backend_dir}/pipeline', 'backend/pipeline'),
|
||||
('{backend_dir}/prompt', 'backend/prompt'),
|
||||
],
|
||||
hiddenimports=[
|
||||
'uvicorn',
|
||||
'fastapi',
|
||||
'celery',
|
||||
'sqlalchemy',
|
||||
'pydantic',
|
||||
'psutil',
|
||||
'requests',
|
||||
'aiofiles',
|
||||
'multipart',
|
||||
'kombu.transport.filesystem',
|
||||
'sqlalchemy.dialects.sqlite',
|
||||
'backend',
|
||||
'backend.app_factory',
|
||||
'backend.desktop_celery',
|
||||
'backend.tasks.processing',
|
||||
]
|
||||
+ collect_submodules('celery')
|
||||
+ collect_submodules('kombu')
|
||||
+ collect_submodules('billiard')
|
||||
+ collect_submodules('vine'),
|
||||
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,
|
||||
[],
|
||||
name='autoclip-backend',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
exclude_binaries=True,
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='autoclip-backend',
|
||||
)
|
||||
"""
|
||||
|
||||
spec_file = backend_dir / "autoclip-backend.spec"
|
||||
with open(spec_file, 'w', encoding='utf-8') as f:
|
||||
f.write(spec_content)
|
||||
|
||||
# 构建后端二进制
|
||||
print("🔨 构建后端二进制...")
|
||||
try:
|
||||
# 清理旧的构建产物
|
||||
print("🧹 清理旧的构建产物...")
|
||||
for stale_dir in [
|
||||
backend_dir / "dist",
|
||||
backend_dir / "build",
|
||||
project_root / "build",
|
||||
]:
|
||||
if stale_dir.exists():
|
||||
shutil.rmtree(stale_dir)
|
||||
stale_dist = project_root / "dist" / "autoclip-backend"
|
||||
if stale_dist.exists():
|
||||
if stale_dist.is_dir():
|
||||
shutil.rmtree(stale_dist)
|
||||
else:
|
||||
stale_dist.unlink()
|
||||
resource_backend = resources_dir / "autoclip-backend"
|
||||
if resource_backend.exists():
|
||||
if resource_backend.is_dir():
|
||||
shutil.rmtree(resource_backend)
|
||||
else:
|
||||
resource_backend.unlink()
|
||||
|
||||
# 在项目根目录执行构建,确保路径正确
|
||||
subprocess.run([
|
||||
str(python_path), "-m", "PyInstaller",
|
||||
"--clean",
|
||||
"--noconfirm",
|
||||
str(spec_file)
|
||||
], check=True, cwd=project_root)
|
||||
|
||||
# 移动构建结果到资源目录
|
||||
# PyInstaller 输出到项目根目录的 dist 文件夹
|
||||
dist_dir = project_root / "dist" / "autoclip-backend"
|
||||
if dist_dir.exists():
|
||||
shutil.copytree(dist_dir, resources_dir / "autoclip-backend")
|
||||
print("✅ 后端二进制构建成功")
|
||||
else:
|
||||
print("❌ 构建文件不存在")
|
||||
return 1
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ 后端二进制构建失败: {e}")
|
||||
return 1
|
||||
|
||||
# 清理临时文件
|
||||
if spec_file.exists():
|
||||
spec_file.unlink()
|
||||
|
||||
print("🎉 桌面版后端构建完成!")
|
||||
print(f"📁 二进制文件位置: {resources_dir / 'autoclip-backend'}")
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
构建完整的桌面应用 - 桌面版
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
def main():
|
||||
print("🚀 开始构建桌面应用...")
|
||||
|
||||
# 设置路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
frontend_dir = project_root / "frontend"
|
||||
src_tauri_dir = project_root / "src-tauri"
|
||||
|
||||
# 检查必要工具
|
||||
print("🔍 检查构建环境...")
|
||||
|
||||
# 检查Node.js
|
||||
try:
|
||||
result = subprocess.run(["node", "--version"], capture_output=True, text=True)
|
||||
print(f"✅ Node.js版本: {result.stdout.strip()}")
|
||||
except FileNotFoundError:
|
||||
print("❌ Node.js未安装")
|
||||
return 1
|
||||
|
||||
# 检查npm
|
||||
try:
|
||||
result = subprocess.run(["npm", "--version"], capture_output=True, text=True)
|
||||
print(f"✅ npm版本: {result.stdout.strip()}")
|
||||
except FileNotFoundError:
|
||||
print("❌ npm未安装")
|
||||
return 1
|
||||
|
||||
# 检查Rust
|
||||
try:
|
||||
result = subprocess.run(["rustc", "--version"], capture_output=True, text=True)
|
||||
print(f"✅ Rust版本: {result.stdout.strip()}")
|
||||
except FileNotFoundError:
|
||||
print("❌ Rust未安装")
|
||||
return 1
|
||||
|
||||
# 检查Tauri CLI
|
||||
try:
|
||||
result = subprocess.run(["tauri", "--version"], capture_output=True, text=True)
|
||||
print(f"✅ Tauri CLI版本: {result.stdout.strip()}")
|
||||
except FileNotFoundError:
|
||||
print("❌ Tauri CLI未安装,正在安装...")
|
||||
try:
|
||||
subprocess.run(["npm", "install", "-g", "@tauri-apps/cli"], check=True)
|
||||
print("✅ Tauri CLI安装成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Tauri CLI安装失败: {e}")
|
||||
return 1
|
||||
|
||||
# 1. 构建后端
|
||||
print("\n🔧 构建后端...")
|
||||
try:
|
||||
result = subprocess.run([sys.executable, "scripts/build_backend.py"],
|
||||
check=True, cwd=project_root)
|
||||
print("✅ 后端构建成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ 后端构建失败: {e}")
|
||||
return 1
|
||||
|
||||
# 2. 安装前端依赖
|
||||
print("\n📦 安装前端依赖...")
|
||||
try:
|
||||
subprocess.run(["npm", "install"], check=True, cwd=frontend_dir)
|
||||
print("✅ 前端依赖安装成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ 前端依赖安装失败: {e}")
|
||||
return 1
|
||||
|
||||
# 3. 构建前端
|
||||
print("\n📦 构建前端...")
|
||||
try:
|
||||
subprocess.run(["npm", "run", "build"], check=True, cwd=frontend_dir)
|
||||
print("✅ 前端构建成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ 前端构建失败: {e}")
|
||||
return 1
|
||||
|
||||
# 4. 构建Tauri应用
|
||||
print("\n🦀 构建Tauri应用...")
|
||||
try:
|
||||
subprocess.run(["tauri", "build"], check=True, cwd=src_tauri_dir)
|
||||
print("✅ Tauri应用构建成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Tauri应用构建失败: {e}")
|
||||
return 1
|
||||
|
||||
print("\n🎉 桌面应用构建完成!")
|
||||
|
||||
# 显示构建结果
|
||||
bundle_dir = src_tauri_dir / "target" / "release" / "bundle"
|
||||
if bundle_dir.exists():
|
||||
print(f"📁 安装包位置: {bundle_dir}")
|
||||
|
||||
# 列出可用的安装包
|
||||
for item in bundle_dir.iterdir():
|
||||
if item.is_dir():
|
||||
print(f" 📦 {item.name}/")
|
||||
for file in item.iterdir():
|
||||
if file.is_file():
|
||||
size = file.stat().st_size / (1024 * 1024) # MB
|
||||
print(f" 📄 {file.name} ({size:.1f} MB)")
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,223 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
发布版本构建脚本
|
||||
用于构建所有平台的桌面应用发布版本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
def get_version():
|
||||
"""获取当前版本号"""
|
||||
try:
|
||||
with open("src-tauri/Cargo.toml", "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("version ="):
|
||||
return line.split('"')[1]
|
||||
except:
|
||||
return "1.0.0"
|
||||
|
||||
def clean_build_artifacts():
|
||||
"""清理构建产物"""
|
||||
print("🧹 清理构建产物...")
|
||||
|
||||
paths_to_clean = [
|
||||
"dist/",
|
||||
"build/",
|
||||
"src-tauri/target/",
|
||||
"frontend/dist/",
|
||||
"src-tauri/resources/autoclip-backend",
|
||||
"src-tauri/resources/autoclip-backend-macos-arm64",
|
||||
]
|
||||
|
||||
for path in paths_to_clean:
|
||||
if os.path.exists(path):
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
print(f" ✓ 删除目录: {path}")
|
||||
else:
|
||||
os.remove(path)
|
||||
print(f" ✓ 删除文件: {path}")
|
||||
|
||||
def build_frontend():
|
||||
"""构建前端"""
|
||||
print("🎨 构建前端...")
|
||||
|
||||
try:
|
||||
subprocess.run(["npm", "run", "build"], cwd="frontend", check=True)
|
||||
print(" ✓ 前端构建成功")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f" ❌ 前端构建失败: {e}")
|
||||
return False
|
||||
|
||||
def prepare_backend_resources():
|
||||
"""准备后端资源"""
|
||||
print("🔧 准备后端资源...")
|
||||
|
||||
try:
|
||||
subprocess.run([sys.executable, "prepare_resources.py"], cwd="scripts", check=True)
|
||||
print(" ✓ 后端资源准备完成")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f" ❌ 后端资源准备失败: {e}")
|
||||
return False
|
||||
|
||||
def build_desktop_app():
|
||||
"""构建桌面应用"""
|
||||
print("🖥️ 构建桌面应用...")
|
||||
|
||||
try:
|
||||
# 构建所有平台
|
||||
subprocess.run(["npm", "run", "tauri", "build"], cwd="src-tauri", check=True)
|
||||
print(" ✓ 桌面应用构建成功")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f" ❌ 桌面应用构建失败: {e}")
|
||||
return False
|
||||
|
||||
def create_release_package():
|
||||
"""创建发布包"""
|
||||
print("📦 创建发布包...")
|
||||
|
||||
version = get_version()
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# 创建发布目录
|
||||
release_dir = Path(f"releases/v{version}_{timestamp}")
|
||||
release_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 复制构建产物
|
||||
dist_dir = Path("src-tauri/target/release/bundle")
|
||||
if dist_dir.exists():
|
||||
for item in dist_dir.iterdir():
|
||||
if item.is_dir():
|
||||
shutil.copytree(item, release_dir / item.name)
|
||||
print(f" ✓ 复制: {item.name}")
|
||||
|
||||
# 复制文档
|
||||
docs_to_copy = [
|
||||
"README.md",
|
||||
"RELEASE_NOTES.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"env.example"
|
||||
]
|
||||
|
||||
for doc in docs_to_copy:
|
||||
if os.path.exists(doc):
|
||||
shutil.copy2(doc, release_dir)
|
||||
print(f" ✓ 复制文档: {doc}")
|
||||
|
||||
# 创建安装说明
|
||||
install_guide = release_dir / "INSTALL.md"
|
||||
with open(install_guide, "w", encoding="utf-8") as f:
|
||||
f.write(f"""# AutoClip 桌面版安装指南
|
||||
|
||||
## 版本: v{version}
|
||||
## 构建时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 根据您的操作系统选择对应的安装包:
|
||||
- Windows: 选择 .msi 文件
|
||||
- macOS: 选择 .dmg 文件
|
||||
- Linux: 选择 .AppImage 或 .deb 文件
|
||||
|
||||
2. 运行安装包并按照提示完成安装
|
||||
|
||||
3. 首次启动时,系统会自动初始化
|
||||
|
||||
4. 在设置中配置通义千问API密钥(参考 env.example)
|
||||
|
||||
### 系统要求
|
||||
|
||||
- Windows 10+ / macOS 10.13+ / Linux
|
||||
- 4GB+ RAM
|
||||
- 10GB+ 可用存储空间
|
||||
- 稳定的网络连接
|
||||
|
||||
### 获取帮助
|
||||
|
||||
- 查看 README.md 了解详细使用说明
|
||||
- 查看 RELEASE_NOTES.md 了解新功能
|
||||
- 提交问题到: https://github.com/zhouxiaoka/autoclip/issues
|
||||
|
||||
### 许可证
|
||||
|
||||
本项目采用 MIT 许可证,详见 LICENSE 文件。
|
||||
""")
|
||||
|
||||
print(f" ✓ 发布包创建完成: {release_dir}")
|
||||
return release_dir
|
||||
|
||||
def generate_checksums(release_dir):
|
||||
"""生成校验和"""
|
||||
print("🔐 生成校验和...")
|
||||
|
||||
checksums = []
|
||||
for file_path in release_dir.rglob("*"):
|
||||
if file_path.is_file():
|
||||
try:
|
||||
import hashlib
|
||||
with open(file_path, "rb") as f:
|
||||
sha256 = hashlib.sha256(f.read()).hexdigest()
|
||||
relative_path = file_path.relative_to(release_dir)
|
||||
checksums.append(f"{sha256} {relative_path}")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 跳过文件 {file_path}: {e}")
|
||||
|
||||
checksum_file = release_dir / "checksums.txt"
|
||||
with open(checksum_file, "w") as f:
|
||||
f.write("\n".join(checksums))
|
||||
|
||||
print(f" ✓ 校验和文件: {checksum_file}")
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("🚀 开始构建发布版本...")
|
||||
print(f"版本: {get_version()}")
|
||||
print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# 检查环境
|
||||
if not os.path.exists("package.json"):
|
||||
print("❌ 请在项目根目录运行此脚本")
|
||||
return 1
|
||||
|
||||
# 构建流程
|
||||
steps = [
|
||||
("清理构建产物", clean_build_artifacts),
|
||||
("构建前端", build_frontend),
|
||||
("准备后端资源", prepare_backend_resources),
|
||||
("构建桌面应用", build_desktop_app),
|
||||
("创建发布包", create_release_package),
|
||||
]
|
||||
|
||||
for step_name, step_func in steps:
|
||||
print(f"\n📋 {step_name}...")
|
||||
if not step_func():
|
||||
print(f"❌ {step_name}失败,构建终止")
|
||||
return 1
|
||||
|
||||
# 生成校验和
|
||||
release_dir = create_release_package()
|
||||
generate_checksums(release_dir)
|
||||
|
||||
print("\n🎉 发布版本构建完成!")
|
||||
print(f"📦 发布包位置: {release_dir}")
|
||||
print("\n📋 下一步:")
|
||||
print("1. 测试发布包在不同平台上的安装和运行")
|
||||
print("2. 创建 GitHub Release")
|
||||
print("3. 上传发布包到 GitHub Releases")
|
||||
print("4. 更新项目文档")
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
安装包体积检查脚本
|
||||
用于检查 Tauri 构建产物的体积,确保不超过预设阈值
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
class Colors:
|
||||
RED = '\033[0;31m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[1;33m'
|
||||
BLUE = '\033[0;34m'
|
||||
NC = '\033[0m' # No Color
|
||||
|
||||
class BundleSizeChecker:
|
||||
def __init__(self, project_root: Path, max_size_mb: int = 500):
|
||||
self.project_root = project_root
|
||||
self.max_size_mb = max_size_mb
|
||||
self.max_size_bytes = max_size_mb * 1024 * 1024
|
||||
|
||||
# Tauri 构建产物路径
|
||||
self.bundle_paths = [
|
||||
project_root / "src-tauri" / "target" / "release" / "bundle",
|
||||
project_root / "dist",
|
||||
project_root / "build"
|
||||
]
|
||||
|
||||
def log(self, message: str, color: str = Colors.NC):
|
||||
"""打印带颜色的日志"""
|
||||
print(f"{color}{message}{Colors.NC}")
|
||||
|
||||
def get_file_size(self, file_path: Path) -> int:
|
||||
"""获取文件大小(字节)"""
|
||||
try:
|
||||
return file_path.stat().st_size
|
||||
except (OSError, FileNotFoundError):
|
||||
return 0
|
||||
|
||||
def get_dir_size(self, dir_path: Path) -> int:
|
||||
"""获取目录总大小(字节)"""
|
||||
total_size = 0
|
||||
try:
|
||||
for item in dir_path.rglob('*'):
|
||||
if item.is_file():
|
||||
total_size += self.get_file_size(item)
|
||||
except (OSError, FileNotFoundError):
|
||||
pass
|
||||
return total_size
|
||||
|
||||
def format_size(self, size_bytes: int) -> str:
|
||||
"""格式化文件大小"""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
elif size_bytes < 1024 * 1024 * 1024:
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
||||
|
||||
def find_bundle_files(self) -> List[Tuple[Path, int]]:
|
||||
"""查找所有构建产物文件"""
|
||||
bundle_files = []
|
||||
|
||||
for bundle_path in self.bundle_paths:
|
||||
if bundle_path.exists():
|
||||
if bundle_path.is_file():
|
||||
size = self.get_file_size(bundle_path)
|
||||
bundle_files.append((bundle_path, size))
|
||||
elif bundle_path.is_dir():
|
||||
# 查找常见的安装包文件
|
||||
for pattern in ["*.dmg", "*.pkg", "*.deb", "*.rpm", "*.msi", "*.exe", "*.app"]:
|
||||
for file_path in bundle_path.rglob(pattern):
|
||||
if file_path.is_file():
|
||||
size = self.get_file_size(file_path)
|
||||
bundle_files.append((file_path, size))
|
||||
|
||||
return bundle_files
|
||||
|
||||
def check_resource_size(self) -> Dict[str, int]:
|
||||
"""检查资源文件大小"""
|
||||
resources_path = self.project_root / "src-tauri" / "resources"
|
||||
resource_sizes = {}
|
||||
|
||||
if not resources_path.exists():
|
||||
return resource_sizes
|
||||
|
||||
for item in resources_path.rglob('*'):
|
||||
if item.is_file():
|
||||
size = self.get_file_size(item)
|
||||
relative_path = item.relative_to(resources_path)
|
||||
resource_sizes[str(relative_path)] = size
|
||||
|
||||
return resource_sizes
|
||||
|
||||
def run_check(self, verbose: bool = False) -> bool:
|
||||
"""运行体积检查"""
|
||||
self.log("🔍 开始检查安装包体积", Colors.YELLOW)
|
||||
self.log(f"最大允许体积: {self.format_size(self.max_size_bytes)}", Colors.YELLOW)
|
||||
self.log("")
|
||||
|
||||
# 检查构建产物
|
||||
bundle_files = self.find_bundle_files()
|
||||
|
||||
if not bundle_files:
|
||||
self.log("⚠️ 未找到构建产物文件", Colors.YELLOW)
|
||||
self.log("请确保已经运行过构建命令", Colors.YELLOW)
|
||||
return True
|
||||
|
||||
total_size = 0
|
||||
oversized_files = []
|
||||
|
||||
self.log("📦 构建产物文件:", Colors.BLUE)
|
||||
for file_path, size in bundle_files:
|
||||
total_size += size
|
||||
size_str = self.format_size(size)
|
||||
|
||||
if size > self.max_size_bytes:
|
||||
oversized_files.append((file_path, size))
|
||||
self.log(f" ❌ {file_path.name}: {size_str} (超过限制)", Colors.RED)
|
||||
else:
|
||||
self.log(f" ✅ {file_path.name}: {size_str}", Colors.GREEN)
|
||||
|
||||
self.log("")
|
||||
self.log(f"📊 总大小: {self.format_size(total_size)}", Colors.YELLOW)
|
||||
|
||||
# 检查资源文件
|
||||
if verbose:
|
||||
self.log("")
|
||||
self.log("📁 资源文件详情:", Colors.BLUE)
|
||||
resource_sizes = self.check_resource_size()
|
||||
|
||||
# 按大小排序
|
||||
sorted_resources = sorted(resource_sizes.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
for relative_path, size in sorted_resources:
|
||||
size_str = self.format_size(size)
|
||||
self.log(f" {relative_path}: {size_str}")
|
||||
|
||||
# 检查结果
|
||||
if oversized_files:
|
||||
self.log("")
|
||||
self.log("❌ 体积检查失败!", Colors.RED)
|
||||
self.log("以下文件超过体积限制:", Colors.RED)
|
||||
for file_path, size in oversized_files:
|
||||
size_str = self.format_size(size)
|
||||
self.log(f" - {file_path.name}: {size_str}", Colors.RED)
|
||||
return False
|
||||
elif total_size > self.max_size_bytes:
|
||||
self.log("")
|
||||
self.log("❌ 体积检查失败!", Colors.RED)
|
||||
self.log(f"总大小 {self.format_size(total_size)} 超过限制 {self.format_size(self.max_size_bytes)}", Colors.RED)
|
||||
return False
|
||||
else:
|
||||
self.log("")
|
||||
self.log("✅ 体积检查通过!", Colors.GREEN)
|
||||
return True
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='安装包体积检查脚本')
|
||||
parser.add_argument('-m', '--max-size', type=int, default=500, help='最大允许体积(MB,默认: 500)')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='详细输出')
|
||||
parser.add_argument('--project-root', type=str, help='项目根目录路径')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 确定项目根目录
|
||||
if args.project_root:
|
||||
project_root = Path(args.project_root)
|
||||
else:
|
||||
# 从脚本位置推断项目根目录
|
||||
script_dir = Path(__file__).parent
|
||||
project_root = script_dir.parent
|
||||
|
||||
if not project_root.exists():
|
||||
print(f"错误: 项目根目录不存在: {project_root}")
|
||||
sys.exit(1)
|
||||
|
||||
# 创建检查器并运行检查
|
||||
checker = BundleSizeChecker(project_root, args.max_size)
|
||||
success = checker.run_check(args.verbose)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,274 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
桌面应用开发环境管理脚本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import signal
|
||||
import time
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
class DesktopDevManager:
|
||||
def __init__(self):
|
||||
self.project_root = Path(__file__).parent.parent
|
||||
self.backend_dir = self.project_root / "backend"
|
||||
self.frontend_dir = self.project_root / "frontend"
|
||||
self.src_tauri_dir = self.project_root / "src-tauri"
|
||||
self.pid_file = self.project_root / "dev_processes.json"
|
||||
self.processes: Dict[str, int] = {}
|
||||
|
||||
def load_processes(self):
|
||||
"""加载进程信息"""
|
||||
if self.pid_file.exists():
|
||||
try:
|
||||
with open(self.pid_file, 'r') as f:
|
||||
self.processes = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ 加载进程信息失败: {e}")
|
||||
self.processes = {}
|
||||
|
||||
def save_processes(self):
|
||||
"""保存进程信息"""
|
||||
try:
|
||||
with open(self.pid_file, 'w') as f:
|
||||
json.dump(self.processes, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"⚠️ 保存进程信息失败: {e}")
|
||||
|
||||
def start_backend(self):
|
||||
"""启动后端服务"""
|
||||
print("🚀 启动后端服务...")
|
||||
|
||||
# 设置环境变量
|
||||
env = os.environ.copy()
|
||||
env["AUTOCLIP_DESKTOP_MODE"] = "true"
|
||||
env["AUTOCLIP_MODE"] = "desktop"
|
||||
|
||||
try:
|
||||
# 启动后端
|
||||
process = subprocess.Popen(
|
||||
[sys.executable, "desktop_main.py"],
|
||||
cwd=self.backend_dir,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
self.processes["backend"] = process.pid
|
||||
self.save_processes()
|
||||
|
||||
print(f"✅ 后端服务已启动 (PID: {process.pid})")
|
||||
print(f"🌐 服务地址: http://127.0.0.1:8000")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 启动后端服务失败: {e}")
|
||||
return False
|
||||
|
||||
def start_frontend(self):
|
||||
"""启动前端开发服务器"""
|
||||
print("🚀 启动前端开发服务器...")
|
||||
|
||||
try:
|
||||
# 启动前端开发服务器
|
||||
process = subprocess.Popen(
|
||||
["npm", "run", "dev"],
|
||||
cwd=self.frontend_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
self.processes["frontend"] = process.pid
|
||||
self.save_processes()
|
||||
|
||||
print(f"✅ 前端开发服务器已启动 (PID: {process.pid})")
|
||||
print(f"🌐 前端地址: http://localhost:5173")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 启动前端开发服务器失败: {e}")
|
||||
return False
|
||||
|
||||
def start_tauri(self):
|
||||
"""启动Tauri开发模式"""
|
||||
print("🚀 启动Tauri开发模式...")
|
||||
|
||||
try:
|
||||
# 启动Tauri开发模式
|
||||
process = subprocess.Popen(
|
||||
["npm", "run", "tauri:dev"],
|
||||
cwd=self.src_tauri_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
self.processes["tauri"] = process.pid
|
||||
self.save_processes()
|
||||
|
||||
print(f"✅ Tauri开发模式已启动 (PID: {process.pid})")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 启动Tauri开发模式失败: {e}")
|
||||
return False
|
||||
|
||||
def stop_process(self, name: str):
|
||||
"""停止指定进程"""
|
||||
if name not in self.processes:
|
||||
print(f"⚠️ 进程 {name} 未运行")
|
||||
return
|
||||
|
||||
pid = self.processes[name]
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
print(f"✅ 进程 {name} 已停止 (PID: {pid})")
|
||||
del self.processes[name]
|
||||
self.save_processes()
|
||||
except ProcessLookupError:
|
||||
print(f"⚠️ 进程 {name} (PID: {pid}) 不存在")
|
||||
del self.processes[name]
|
||||
self.save_processes()
|
||||
except Exception as e:
|
||||
print(f"❌ 停止进程 {name} 失败: {e}")
|
||||
|
||||
def stop_all(self):
|
||||
"""停止所有进程"""
|
||||
print("🛑 停止所有开发进程...")
|
||||
|
||||
for name in list(self.processes.keys()):
|
||||
self.stop_process(name)
|
||||
|
||||
# 清理PID文件
|
||||
if self.pid_file.exists():
|
||||
self.pid_file.unlink()
|
||||
|
||||
print("✅ 所有进程已停止")
|
||||
|
||||
def status(self):
|
||||
"""显示服务状态"""
|
||||
print("📊 开发环境状态:")
|
||||
print("-" * 50)
|
||||
|
||||
if not self.processes:
|
||||
print("❌ 没有运行中的进程")
|
||||
return
|
||||
|
||||
for name, pid in self.processes.items():
|
||||
try:
|
||||
os.kill(pid, 0) # 检查进程是否存在
|
||||
print(f"✅ {name}: 运行中 (PID: {pid})")
|
||||
except ProcessLookupError:
|
||||
print(f"❌ {name}: 已停止 (PID: {pid})")
|
||||
del self.processes[name]
|
||||
|
||||
self.save_processes()
|
||||
|
||||
# 检查服务健康状态
|
||||
print("\n🔍 服务健康检查:")
|
||||
try:
|
||||
import requests
|
||||
response = requests.get("http://127.0.0.1:8000/health", timeout=5)
|
||||
if response.status_code == 200:
|
||||
print("✅ 后端服务健康")
|
||||
else:
|
||||
print("⚠️ 后端服务响应异常")
|
||||
except Exception as e:
|
||||
print(f"❌ 后端服务不可用: {e}")
|
||||
|
||||
def install_dependencies(self):
|
||||
"""安装依赖"""
|
||||
print("📦 安装依赖...")
|
||||
|
||||
# 安装Python依赖
|
||||
print("安装Python依赖...")
|
||||
try:
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"],
|
||||
check=True, cwd=self.project_root)
|
||||
print("✅ Python依赖安装成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Python依赖安装失败: {e}")
|
||||
return False
|
||||
|
||||
# 安装前端依赖
|
||||
print("安装前端依赖...")
|
||||
try:
|
||||
subprocess.run(["npm", "install"], check=True, cwd=self.frontend_dir)
|
||||
print("✅ 前端依赖安装成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ 前端依赖安装失败: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python dev_desktop.py <command>")
|
||||
print("命令:")
|
||||
print(" start - 启动开发环境")
|
||||
print(" stop - 停止开发环境")
|
||||
print(" restart - 重启开发环境")
|
||||
print(" status - 显示状态")
|
||||
print(" install - 安装依赖")
|
||||
return 1
|
||||
|
||||
manager = DesktopDevManager()
|
||||
manager.load_processes()
|
||||
|
||||
command = sys.argv[1].lower()
|
||||
|
||||
if command == "start":
|
||||
print("🚀 启动桌面应用开发环境...")
|
||||
|
||||
# 安装依赖
|
||||
if not manager.install_dependencies():
|
||||
return 1
|
||||
|
||||
# 启动服务
|
||||
success = True
|
||||
success &= manager.start_backend()
|
||||
time.sleep(2) # 等待后端启动
|
||||
success &= manager.start_frontend()
|
||||
time.sleep(2) # 等待前端启动
|
||||
success &= manager.start_tauri()
|
||||
|
||||
if success:
|
||||
print("\n🎉 开发环境启动成功!")
|
||||
print("📱 桌面应用将自动打开")
|
||||
print("🌐 前端开发服务器: http://localhost:5173")
|
||||
print("🔧 后端API服务: http://127.0.0.1:8000")
|
||||
else:
|
||||
print("\n❌ 开发环境启动失败")
|
||||
return 1
|
||||
|
||||
elif command == "stop":
|
||||
manager.stop_all()
|
||||
|
||||
elif command == "restart":
|
||||
print("🔄 重启开发环境...")
|
||||
manager.stop_all()
|
||||
time.sleep(2)
|
||||
return main() # 递归调用start
|
||||
|
||||
elif command == "status":
|
||||
manager.status()
|
||||
|
||||
elif command == "install":
|
||||
if not manager.install_dependencies():
|
||||
return 1
|
||||
print("✅ 依赖安装完成")
|
||||
|
||||
else:
|
||||
print(f"❌ 未知命令: {command}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,175 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
资源准备脚本 - 只打包当前平台所需的文件
|
||||
避免将三平台 ffmpeg 和大量不必要文件打入安装包
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
def get_platform_name():
|
||||
"""获取当前平台名称"""
|
||||
system = platform.system().lower()
|
||||
if system == "darwin":
|
||||
return "macos"
|
||||
elif system == "windows":
|
||||
return "windows"
|
||||
elif system == "linux":
|
||||
return "linux"
|
||||
else:
|
||||
raise ValueError(f"不支持的平台: {system}")
|
||||
|
||||
def prepare_resources():
|
||||
"""准备打包资源"""
|
||||
# 获取项目根目录
|
||||
project_root = Path(__file__).parent.parent
|
||||
tauri_resources = project_root / "src-tauri" / "resources"
|
||||
|
||||
print(f"项目根目录: {project_root}")
|
||||
print(f"Tauri 资源目录: {tauri_resources}")
|
||||
|
||||
# 清理旧的资源文件(保留 ffmpeg-unified)
|
||||
if tauri_resources.exists():
|
||||
print("清理旧的资源文件...")
|
||||
# 先备份 ffmpeg-unified 目录
|
||||
ffmpeg_unified_backup = None
|
||||
ffmpeg_unified_dir = tauri_resources / "ffmpeg-unified"
|
||||
if ffmpeg_unified_dir.exists():
|
||||
ffmpeg_unified_backup = project_root / "temp_ffmpeg_unified"
|
||||
if ffmpeg_unified_backup.exists():
|
||||
shutil.rmtree(ffmpeg_unified_backup)
|
||||
shutil.move(str(ffmpeg_unified_dir), str(ffmpeg_unified_backup))
|
||||
|
||||
shutil.rmtree(tauri_resources)
|
||||
|
||||
# 恢复 ffmpeg-unified 目录
|
||||
if ffmpeg_unified_backup and ffmpeg_unified_backup.exists():
|
||||
tauri_resources.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(str(ffmpeg_unified_backup), str(ffmpeg_unified_dir))
|
||||
|
||||
# 创建资源目录
|
||||
tauri_resources.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 获取当前平台
|
||||
current_platform = get_platform_name()
|
||||
print(f"当前平台: {current_platform}")
|
||||
|
||||
# 不复制后端二进制文件(太大,改用 Python 源代码)
|
||||
# 桌面端将在运行时创建 Python 环境
|
||||
print("ℹ 跳过后端二进制文件(体积过大),将使用 Python 源代码")
|
||||
|
||||
# 只复制当前平台的 ffmpeg
|
||||
ffmpeg_source_dir = project_root / "src-tauri" / "resources" / "ffmpeg-unified"
|
||||
if ffmpeg_source_dir.exists():
|
||||
# 创建 ffmpeg 目录
|
||||
ffmpeg_dest_dir = tauri_resources / "ffmpeg"
|
||||
ffmpeg_dest_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 根据平台选择对应的 ffmpeg 文件
|
||||
platform_files = {
|
||||
"macos": "ffmpeg-macos",
|
||||
"windows": "ffmpeg-windows.exe",
|
||||
"linux": "ffmpeg-linux"
|
||||
}
|
||||
|
||||
if current_platform in platform_files:
|
||||
source_file = ffmpeg_source_dir / platform_files[current_platform]
|
||||
if source_file.exists():
|
||||
dest_file = ffmpeg_dest_dir / "ffmpeg"
|
||||
shutil.copy2(source_file, dest_file)
|
||||
# 设置执行权限
|
||||
os.chmod(dest_file, 0o755)
|
||||
print(f"✓ 复制 {current_platform} ffmpeg")
|
||||
else:
|
||||
print(f"⚠ {current_platform} 平台的 ffmpeg 文件不存在: {source_file}")
|
||||
else:
|
||||
print(f"⚠ 不支持的平台: {current_platform}")
|
||||
else:
|
||||
print("⚠ ffmpeg 源目录不存在")
|
||||
|
||||
# 复制后端 Python 源代码(排除不必要文件)
|
||||
backend_source = project_root / "backend"
|
||||
if backend_source.exists():
|
||||
backend_dest = tauri_resources / "backend"
|
||||
backend_dest.mkdir(exist_ok=True)
|
||||
|
||||
# 要排除的目录和文件
|
||||
exclude_dirs = {
|
||||
'__pycache__', '.mypy_cache', '.ruff_cache', '.pytest_cache',
|
||||
'tests', 'data', 'logs', 'temp', 'uploads', 'whisper_models'
|
||||
}
|
||||
exclude_files = {'.pyc', '.pyo', '.pyd', '.log', '.pid', '.rdb'}
|
||||
|
||||
def should_exclude(path):
|
||||
if path.name in exclude_dirs:
|
||||
return True
|
||||
if path.suffix in exclude_files:
|
||||
return True
|
||||
return False
|
||||
|
||||
def copy_backend_files(source, dest):
|
||||
for item in source.iterdir():
|
||||
if should_exclude(item):
|
||||
continue
|
||||
|
||||
dest_item = dest / item.name
|
||||
if item.is_dir():
|
||||
dest_item.mkdir(exist_ok=True)
|
||||
copy_backend_files(item, dest_item)
|
||||
else:
|
||||
shutil.copy2(item, dest_item)
|
||||
|
||||
copy_backend_files(backend_source, backend_dest)
|
||||
print("✓ 复制后端 Python 源代码")
|
||||
|
||||
# 复制 requirements.txt
|
||||
requirements_file = project_root / "requirements.txt"
|
||||
if requirements_file.exists():
|
||||
shutil.copy2(requirements_file, tauri_resources / "requirements.txt")
|
||||
print("✓ 复制 requirements.txt")
|
||||
else:
|
||||
print("⚠ 后端源代码目录不存在")
|
||||
|
||||
# 复制 uv(Python 包管理器)
|
||||
uv_source = project_root / "uv"
|
||||
if uv_source.exists():
|
||||
shutil.copy2(uv_source, tauri_resources / "uv")
|
||||
os.chmod(tauri_resources / "uv", 0o755)
|
||||
print("✓ 复制 uv")
|
||||
else:
|
||||
print("⚠ uv 文件不存在,将使用系统 Python")
|
||||
|
||||
print(f"\n资源准备完成!")
|
||||
print(f"资源目录大小: {get_dir_size(tauri_resources):.2f} MB")
|
||||
|
||||
# 列出最终的文件结构
|
||||
print("\n最终资源文件结构:")
|
||||
for root, dirs, files in os.walk(tauri_resources):
|
||||
level = root.replace(str(tauri_resources), '').count(os.sep)
|
||||
indent = ' ' * 2 * level
|
||||
print(f"{indent}{os.path.basename(root)}/")
|
||||
subindent = ' ' * 2 * (level + 1)
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
size = file_path.stat().st_size / (1024 * 1024) # MB
|
||||
print(f"{subindent}{file} ({size:.2f} MB)")
|
||||
|
||||
def get_dir_size(path):
|
||||
"""获取目录大小(MB)"""
|
||||
total_size = 0
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
for filename in filenames:
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
if os.path.exists(filepath):
|
||||
total_size += os.path.getsize(filepath)
|
||||
return total_size / (1024 * 1024) # 转换为 MB
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
prepare_resources()
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
sys.exit(1)
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 AutoClip 快速构建自检脚本"
|
||||
echo "================================"
|
||||
|
||||
# 获取项目根目录
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "📁 项目根目录: $PROJECT_ROOT"
|
||||
|
||||
# 1) 清理构建产物
|
||||
echo "🧹 清理构建产物..."
|
||||
rm -rf frontend/dist
|
||||
rm -rf frontend/node_modules/.vite
|
||||
rm -rf src-tauri/target
|
||||
rm -rf src-tauri/resources
|
||||
|
||||
echo "✓ 清理完成"
|
||||
|
||||
# 2) 前端重装 + 生产构建
|
||||
echo "📦 前端依赖安装..."
|
||||
cd frontend
|
||||
npm install
|
||||
echo "✓ 前端依赖安装完成"
|
||||
|
||||
echo "🔨 前端生产构建..."
|
||||
npm run build
|
||||
echo "✓ 前端构建完成"
|
||||
|
||||
# 检查 dist 目录
|
||||
if [ -d "dist" ]; then
|
||||
DIST_SIZE=$(du -sh dist | cut -f1)
|
||||
echo "📊 前端产物大小: $DIST_SIZE"
|
||||
else
|
||||
echo "❌ 前端构建失败,dist 目录不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# 3) 准备资源文件
|
||||
echo "📋 准备资源文件..."
|
||||
python scripts/prepare_resources.py
|
||||
echo "✓ 资源准备完成"
|
||||
|
||||
# 4) 检查 .taurignore
|
||||
echo "📝 检查 .taurignore..."
|
||||
if [ -f ".taurignore" ]; then
|
||||
echo "✓ .taurignore 文件存在"
|
||||
echo "排除的文件类型:"
|
||||
grep -E "^[^#]" .taurignore | head -10
|
||||
else
|
||||
echo "⚠ .taurignore 文件不存在"
|
||||
fi
|
||||
|
||||
# 5) 检查资源目录大小
|
||||
echo "📊 资源目录分析..."
|
||||
if [ -d "src-tauri/resources" ]; then
|
||||
RESOURCES_SIZE=$(du -sh src-tauri/resources | cut -f1)
|
||||
echo "📦 资源目录大小: $RESOURCES_SIZE"
|
||||
|
||||
echo "资源文件列表:"
|
||||
find src-tauri/resources -type f -exec ls -lh {} \; | awk '{print $5, $9}' | head -10
|
||||
else
|
||||
echo "❌ 资源目录不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 6) Release 打包
|
||||
echo "🔨 开始 Release 打包..."
|
||||
cd src-tauri
|
||||
|
||||
# 设置环境变量关闭调试符号
|
||||
export RUSTFLAGS="-C debuginfo=0"
|
||||
|
||||
# 执行 Tauri 构建
|
||||
echo "执行: cargo tauri build"
|
||||
cargo tauri build
|
||||
|
||||
echo "✅ 构建完成!"
|
||||
|
||||
# 7) 查看最终安装包体积
|
||||
echo "📊 最终安装包分析..."
|
||||
if [ -d "target/release/bundle" ]; then
|
||||
echo "安装包文件:"
|
||||
find target/release/bundle -name "*.dmg" -o -name "*.app" -o -name "*.exe" -o -name "*.deb" -o -name "*.rpm" | while read file; do
|
||||
if [ -f "$file" ]; then
|
||||
size=$(du -sh "$file" | cut -f1)
|
||||
echo " 📦 $file: $size"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "🎉 构建成功!安装包已生成在 target/release/bundle/ 目录"
|
||||
else
|
||||
echo "❌ 构建失败,bundle 目录不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✨ 快速自检完成!"
|
||||
echo "如果安装包体积仍然很大,请检查:"
|
||||
echo "1. 是否还有大文件被打包进去"
|
||||
echo "2. .taurignore 是否生效"
|
||||
echo "3. 资源准备脚本是否正确过滤了文件"
|
||||
@@ -1,202 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
API 烟雾测试脚本 (Python 版本)
|
||||
用于验证后端 API 的关键端点是否正常工作
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
from typing import List, Tuple, Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
class Colors:
|
||||
RED = '\033[0;31m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[1;33m'
|
||||
BLUE = '\033[0;34m'
|
||||
NC = '\033[0m' # No Color
|
||||
|
||||
class SmokeTest:
|
||||
def __init__(self, base_url: str, timeout: int = 10, verbose: bool = False):
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.timeout = timeout
|
||||
self.verbose = verbose
|
||||
self.session = requests.Session()
|
||||
self.session.timeout = timeout
|
||||
|
||||
# 测试用例定义
|
||||
self.test_cases = [
|
||||
{
|
||||
'endpoint': '/health',
|
||||
'expected_key': 'status',
|
||||
'description': '根健康检查',
|
||||
'required': True
|
||||
},
|
||||
{
|
||||
'endpoint': '/api/health',
|
||||
'expected_key': 'status',
|
||||
'description': 'API 健康检查',
|
||||
'required': True
|
||||
},
|
||||
{
|
||||
'endpoint': '/api/v1/video-categories',
|
||||
'expected_key': 'categories',
|
||||
'description': '视频分类配置',
|
||||
'required': True
|
||||
},
|
||||
{
|
||||
'endpoint': '/api/v1/projects',
|
||||
'expected_key': 'data',
|
||||
'description': '项目列表',
|
||||
'required': False # 可能为空,但端点应该存在
|
||||
},
|
||||
{
|
||||
'endpoint': '/api/v1/settings',
|
||||
'expected_key': 'current_provider',
|
||||
'description': '设置信息',
|
||||
'required': False
|
||||
},
|
||||
{
|
||||
'endpoint': '/api/v1/settings/desktop-mode',
|
||||
'expected_key': 'is_desktop_mode',
|
||||
'description': '桌面模式检查',
|
||||
'required': False
|
||||
}
|
||||
]
|
||||
|
||||
def log(self, message: str, color: str = Colors.NC):
|
||||
"""打印带颜色的日志"""
|
||||
print(f"{color}{message}{Colors.NC}")
|
||||
|
||||
def test_endpoint(self, test_case: dict) -> Tuple[bool, str]:
|
||||
"""测试单个端点"""
|
||||
endpoint = test_case['endpoint']
|
||||
expected_key = test_case['expected_key']
|
||||
description = test_case['description']
|
||||
required = test_case['required']
|
||||
|
||||
url = urljoin(self.base_url, endpoint)
|
||||
|
||||
if self.verbose:
|
||||
self.log(f"测试: {description}", Colors.YELLOW)
|
||||
self.log(f"URL: {url}", Colors.YELLOW)
|
||||
|
||||
try:
|
||||
response = self.session.get(url)
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = response.json()
|
||||
|
||||
if expected_key and expected_key not in data:
|
||||
error_msg = f"响应中缺少期望的键: {expected_key}"
|
||||
if self.verbose:
|
||||
self.log(f"响应: {data}", Colors.RED)
|
||||
return False, error_msg
|
||||
|
||||
if self.verbose:
|
||||
self.log(f"响应: {data}", Colors.GREEN)
|
||||
return True, "成功"
|
||||
|
||||
except ValueError as e:
|
||||
error_msg = f"响应不是有效的 JSON: {e}"
|
||||
if self.verbose:
|
||||
self.log(f"原始响应: {response.text}", Colors.RED)
|
||||
return False, error_msg
|
||||
else:
|
||||
error_msg = f"HTTP {response.status_code}"
|
||||
if self.verbose:
|
||||
self.log(f"响应: {response.text}", Colors.RED)
|
||||
return False, error_msg
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return False, "请求超时"
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False, "连接失败"
|
||||
except requests.exceptions.RequestException as e:
|
||||
return False, f"请求异常: {e}"
|
||||
|
||||
def run_tests(self) -> Tuple[int, int]:
|
||||
"""运行所有测试"""
|
||||
self.log("🚀 开始 API 烟雾测试", Colors.YELLOW)
|
||||
self.log(f"目标地址: {self.base_url}", Colors.YELLOW)
|
||||
self.log(f"超时时间: {self.timeout}秒", Colors.YELLOW)
|
||||
self.log("")
|
||||
|
||||
total_tests = 0
|
||||
passed_tests = 0
|
||||
failed_tests = []
|
||||
|
||||
self.log("📋 执行测试用例...", Colors.YELLOW)
|
||||
self.log("")
|
||||
|
||||
for test_case in self.test_cases:
|
||||
total_tests += 1
|
||||
success, message = self.test_endpoint(test_case)
|
||||
|
||||
if success:
|
||||
passed_tests += 1
|
||||
self.log(f"✅ {test_case['description']}", Colors.GREEN)
|
||||
else:
|
||||
failed_tests.append((test_case['description'], message))
|
||||
if test_case['required']:
|
||||
self.log(f"❌ {test_case['description']} - {message}", Colors.RED)
|
||||
else:
|
||||
self.log(f"⚠️ {test_case['description']} - {message}", Colors.YELLOW)
|
||||
|
||||
return total_tests, passed_tests, failed_tests
|
||||
|
||||
def print_summary(self, total_tests: int, passed_tests: int, failed_tests: List[Tuple[str, str]]):
|
||||
"""打印测试总结"""
|
||||
self.log("")
|
||||
self.log("📊 测试结果", Colors.YELLOW)
|
||||
self.log(f"总测试数: {total_tests}", Colors.YELLOW)
|
||||
self.log(f"通过: {passed_tests}", Colors.GREEN)
|
||||
self.log(f"失败: {len(failed_tests)}", Colors.RED)
|
||||
|
||||
if total_tests > 0:
|
||||
success_rate = (passed_tests * 100) // total_tests
|
||||
self.log(f"成功率: {success_rate}%", Colors.YELLOW)
|
||||
|
||||
if failed_tests:
|
||||
self.log("")
|
||||
self.log("失败的测试:", Colors.RED)
|
||||
for description, message in failed_tests:
|
||||
self.log(f" - {description}: {message}", Colors.RED)
|
||||
|
||||
if passed_tests == total_tests:
|
||||
self.log("🎉 所有测试通过!API 运行正常", Colors.GREEN)
|
||||
return 0
|
||||
elif success_rate >= 80:
|
||||
self.log("⚠️ 大部分测试通过,但有一些问题", Colors.YELLOW)
|
||||
return 1
|
||||
else:
|
||||
self.log("💥 测试失败率过高,API 可能存在问题", Colors.RED)
|
||||
return 2
|
||||
else:
|
||||
self.log("❌ 没有执行任何测试", Colors.RED)
|
||||
return 3
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='API 烟雾测试脚本')
|
||||
parser.add_argument('base_url', help='API 基础 URL')
|
||||
parser.add_argument('-t', '--timeout', type=int, default=10, help='超时时间(秒,默认: 10)')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='详细输出')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 创建测试实例
|
||||
smoke_test = SmokeTest(args.base_url, args.timeout, args.verbose)
|
||||
|
||||
# 运行测试
|
||||
total_tests, passed_tests, failed_tests = smoke_test.run_tests()
|
||||
|
||||
# 打印总结
|
||||
exit_code = smoke_test.print_summary(total_tests, passed_tests, failed_tests)
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,335 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
桌面应用功能测试脚本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
class DesktopTester:
|
||||
def __init__(self):
|
||||
self.project_root = Path(__file__).parent.parent
|
||||
self.backend_url = "http://127.0.0.1:8000"
|
||||
self.test_results: List[Dict[str, Any]] = []
|
||||
|
||||
def log_test(self, test_name: str, success: bool, message: str = ""):
|
||||
"""记录测试结果"""
|
||||
result = {
|
||||
"test": test_name,
|
||||
"success": success,
|
||||
"message": message,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self.test_results.append(result)
|
||||
|
||||
status = "✅" if success else "❌"
|
||||
print(f"{status} {test_name}: {message}")
|
||||
|
||||
def test_file_structure(self):
|
||||
"""测试文件结构完整性"""
|
||||
print("\n📁 测试文件结构...")
|
||||
|
||||
required_files = [
|
||||
"backend/desktop_main.py",
|
||||
"backend/desktop_celery.py",
|
||||
"backend/core/desktop_config.py",
|
||||
"backend/api/v1/desktop.py",
|
||||
"src-tauri/src/lib.rs",
|
||||
"src-tauri/src/backend_manager.rs",
|
||||
"src-tauri/src/commands.rs",
|
||||
"src-tauri/src/tray.rs",
|
||||
"src-tauri/tauri.conf.json",
|
||||
"src-tauri/Cargo.toml",
|
||||
"frontend/src/components/DesktopSettings.tsx",
|
||||
"frontend/src/components/OfflineModeIndicator.tsx",
|
||||
"scripts/build_backend.py",
|
||||
"scripts/build_desktop.py",
|
||||
"scripts/dev_desktop.py",
|
||||
"scripts/test_desktop.py"
|
||||
]
|
||||
|
||||
missing_files = []
|
||||
for file_path in required_files:
|
||||
full_path = self.project_root / file_path
|
||||
if not full_path.exists():
|
||||
missing_files.append(file_path)
|
||||
|
||||
if missing_files:
|
||||
self.log_test("文件结构检查", False, f"缺失文件: {', '.join(missing_files)}")
|
||||
else:
|
||||
self.log_test("文件结构检查", True, f"所有 {len(required_files)} 个文件都存在")
|
||||
|
||||
def test_python_dependencies(self):
|
||||
"""测试Python依赖"""
|
||||
print("\n🐍 测试Python依赖...")
|
||||
|
||||
required_packages = [
|
||||
"fastapi",
|
||||
"uvicorn",
|
||||
"celery",
|
||||
"sqlalchemy",
|
||||
"pydantic",
|
||||
"psutil",
|
||||
"requests"
|
||||
]
|
||||
|
||||
missing_packages = []
|
||||
for package in required_packages:
|
||||
try:
|
||||
__import__(package)
|
||||
except ImportError:
|
||||
missing_packages.append(package)
|
||||
|
||||
if missing_packages:
|
||||
self.log_test("Python依赖检查", False, f"缺失包: {', '.join(missing_packages)}")
|
||||
else:
|
||||
self.log_test("Python依赖检查", True, f"所有 {len(required_packages)} 个包都已安装")
|
||||
|
||||
def test_node_dependencies(self):
|
||||
"""测试Node.js依赖"""
|
||||
print("\n📦 测试Node.js依赖...")
|
||||
|
||||
try:
|
||||
result = subprocess.run(["node", "--version"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
self.log_test("Node.js检查", True, f"版本: {result.stdout.strip()}")
|
||||
else:
|
||||
self.log_test("Node.js检查", False, "Node.js未安装或不可用")
|
||||
return
|
||||
except FileNotFoundError:
|
||||
self.log_test("Node.js检查", False, "Node.js未安装")
|
||||
return
|
||||
|
||||
try:
|
||||
result = subprocess.run(["npm", "--version"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
self.log_test("npm检查", True, f"版本: {result.stdout.strip()}")
|
||||
else:
|
||||
self.log_test("npm检查", False, "npm未安装或不可用")
|
||||
except FileNotFoundError:
|
||||
self.log_test("npm检查", False, "npm未安装")
|
||||
|
||||
def test_rust_environment(self):
|
||||
"""测试Rust环境"""
|
||||
print("\n🦀 测试Rust环境...")
|
||||
|
||||
try:
|
||||
result = subprocess.run(["rustc", "--version"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
self.log_test("Rust检查", True, f"版本: {result.stdout.strip()}")
|
||||
else:
|
||||
self.log_test("Rust检查", False, "Rust未安装或不可用")
|
||||
return
|
||||
except FileNotFoundError:
|
||||
self.log_test("Rust检查", False, "Rust未安装")
|
||||
return
|
||||
|
||||
try:
|
||||
result = subprocess.run(["cargo", "--version"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
self.log_test("Cargo检查", True, f"版本: {result.stdout.strip()}")
|
||||
else:
|
||||
self.log_test("Cargo检查", False, "Cargo未安装或不可用")
|
||||
except FileNotFoundError:
|
||||
self.log_test("Cargo检查", False, "Cargo未安装")
|
||||
|
||||
def test_tauri_config(self):
|
||||
"""测试Tauri配置"""
|
||||
print("\n⚙️ 测试Tauri配置...")
|
||||
|
||||
tauri_conf_path = self.project_root / "src-tauri" / "tauri.conf.json"
|
||||
if not tauri_conf_path.exists():
|
||||
self.log_test("Tauri配置检查", False, "tauri.conf.json不存在")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(tauri_conf_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# 检查必要配置项
|
||||
required_keys = ["package", "build", "tauri"]
|
||||
missing_keys = [key for key in required_keys if key not in config]
|
||||
|
||||
if missing_keys:
|
||||
self.log_test("Tauri配置检查", False, f"缺失配置项: {', '.join(missing_keys)}")
|
||||
else:
|
||||
self.log_test("Tauri配置检查", True, "配置文件格式正确")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.log_test("Tauri配置检查", False, f"JSON格式错误: {e}")
|
||||
except Exception as e:
|
||||
self.log_test("Tauri配置检查", False, f"读取配置失败: {e}")
|
||||
|
||||
def test_cargo_config(self):
|
||||
"""测试Cargo配置"""
|
||||
print("\n📦 测试Cargo配置...")
|
||||
|
||||
cargo_toml_path = self.project_root / "src-tauri" / "Cargo.toml"
|
||||
if not cargo_toml_path.exists():
|
||||
self.log_test("Cargo配置检查", False, "Cargo.toml不存在")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(cargo_toml_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# 检查必要依赖
|
||||
required_deps = ["tauri", "serde", "tokio", "reqwest"]
|
||||
missing_deps = [dep for dep in required_deps if dep not in content]
|
||||
|
||||
if missing_deps:
|
||||
self.log_test("Cargo配置检查", False, f"缺失依赖: {', '.join(missing_deps)}")
|
||||
else:
|
||||
self.log_test("Cargo配置检查", True, "依赖配置正确")
|
||||
|
||||
except Exception as e:
|
||||
self.log_test("Cargo配置检查", False, f"读取配置失败: {e}")
|
||||
|
||||
def test_backend_api(self):
|
||||
"""测试后端API(需要服务运行)"""
|
||||
print("\n🔧 测试后端API...")
|
||||
|
||||
try:
|
||||
# 测试健康检查端点
|
||||
response = requests.get(f"{self.backend_url}/health", timeout=5)
|
||||
if response.status_code == 200:
|
||||
self.log_test("后端健康检查", True, "服务正常运行")
|
||||
else:
|
||||
self.log_test("后端健康检查", False, f"HTTP {response.status_code}")
|
||||
return
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.log_test("后端健康检查", False, "无法连接到后端服务")
|
||||
return
|
||||
except Exception as e:
|
||||
self.log_test("后端健康检查", False, f"请求失败: {e}")
|
||||
return
|
||||
|
||||
# 测试桌面API端点
|
||||
desktop_endpoints = [
|
||||
"/api/v1/desktop/system/info",
|
||||
"/api/v1/desktop/service/status",
|
||||
"/api/v1/desktop/config"
|
||||
]
|
||||
|
||||
for endpoint in desktop_endpoints:
|
||||
try:
|
||||
response = requests.get(f"{self.backend_url}{endpoint}", timeout=5)
|
||||
if response.status_code == 200:
|
||||
self.log_test(f"API端点 {endpoint}", True, "响应正常")
|
||||
else:
|
||||
self.log_test(f"API端点 {endpoint}", False, f"HTTP {response.status_code}")
|
||||
except Exception as e:
|
||||
self.log_test(f"API端点 {endpoint}", False, f"请求失败: {e}")
|
||||
|
||||
def test_frontend_build(self):
|
||||
"""测试前端构建"""
|
||||
print("\n🌐 测试前端构建...")
|
||||
|
||||
frontend_dir = self.project_root / "frontend"
|
||||
if not frontend_dir.exists():
|
||||
self.log_test("前端构建测试", False, "frontend目录不存在")
|
||||
return
|
||||
|
||||
try:
|
||||
# 检查package.json
|
||||
package_json = frontend_dir / "package.json"
|
||||
if not package_json.exists():
|
||||
self.log_test("前端构建测试", False, "package.json不存在")
|
||||
return
|
||||
|
||||
# 尝试构建前端
|
||||
result = subprocess.run(
|
||||
["npm", "run", "build"],
|
||||
cwd=frontend_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300 # 5分钟超时
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.log_test("前端构建测试", True, "构建成功")
|
||||
else:
|
||||
self.log_test("前端构建测试", False, f"构建失败: {result.stderr}")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.log_test("前端构建测试", False, "构建超时")
|
||||
except Exception as e:
|
||||
self.log_test("前端构建测试", False, f"构建异常: {e}")
|
||||
|
||||
def run_all_tests(self):
|
||||
"""运行所有测试"""
|
||||
print("🧪 开始桌面应用功能测试...")
|
||||
print("=" * 60)
|
||||
|
||||
# 基础环境测试
|
||||
self.test_file_structure()
|
||||
self.test_python_dependencies()
|
||||
self.test_node_dependencies()
|
||||
self.test_rust_environment()
|
||||
self.test_tauri_config()
|
||||
self.test_cargo_config()
|
||||
|
||||
# 构建测试
|
||||
self.test_frontend_build()
|
||||
|
||||
# API测试(可选,需要服务运行)
|
||||
print("\n⚠️ 注意: 以下测试需要后端服务运行")
|
||||
print("如果后端服务未运行,这些测试将失败")
|
||||
self.test_backend_api()
|
||||
|
||||
# 输出测试结果
|
||||
self.print_summary()
|
||||
|
||||
def print_summary(self):
|
||||
"""打印测试摘要"""
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 测试结果摘要")
|
||||
print("=" * 60)
|
||||
|
||||
total_tests = len(self.test_results)
|
||||
passed_tests = sum(1 for result in self.test_results if result["success"])
|
||||
failed_tests = total_tests - passed_tests
|
||||
|
||||
print(f"总测试数: {total_tests}")
|
||||
print(f"通过: {passed_tests} ✅")
|
||||
print(f"失败: {failed_tests} ❌")
|
||||
print(f"成功率: {(passed_tests/total_tests*100):.1f}%")
|
||||
|
||||
if failed_tests > 0:
|
||||
print("\n❌ 失败的测试:")
|
||||
for result in self.test_results:
|
||||
if not result["success"]:
|
||||
print(f" - {result['test']}: {result['message']}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--help":
|
||||
print("桌面应用功能测试脚本")
|
||||
print("用法: python test_desktop.py")
|
||||
print("测试项目:")
|
||||
print(" - 文件结构完整性")
|
||||
print(" - Python依赖检查")
|
||||
print(" - Node.js依赖检查")
|
||||
print(" - Rust环境检查")
|
||||
print(" - Tauri配置验证")
|
||||
print(" - Cargo配置验证")
|
||||
print(" - 前端构建测试")
|
||||
print(" - 后端API测试(需要服务运行)")
|
||||
return 0
|
||||
|
||||
tester = DesktopTester()
|
||||
tester.run_all_tests()
|
||||
|
||||
# 返回失败测试数量作为退出码
|
||||
failed_count = sum(1 for result in tester.test_results if not result["success"])
|
||||
return min(failed_count, 255) # 限制退出码范围
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user