From 58588c939560913f3634f6e6dc769e3163b77461 Mon Sep 17 00:00:00 2001 From: QoderAI Date: Sat, 27 Sep 2025 06:15:09 +0000 Subject: [PATCH] feat: add pyinstaller support and json config Co-authored-by: Taowd --- Makefile | 132 ++++++++++ PYINSTALLER_GUIDE.md | 399 +++++++++++++++++++++++++++++ README.md | 219 +++++++++++++++- build/linux/hello-scan-code.spec | 150 +++++++++++ build/windows/hello-scan-code.spec | 143 +++++++++++ config/config.template.json | 35 +++ config/default.json | 32 +++ config/example.json | 42 +++ pyproject.toml | 1 + scripts/build_linux.py | 334 ++++++++++++++++++++++++ scripts/build_windows.py | 279 ++++++++++++++++++++ src/config.py | 87 +++++-- src/config/__init__.py | 16 ++ src/config/config_loader.py | 216 ++++++++++++++++ src/config/config_validator.py | 228 +++++++++++++++++ src/config/default_config.py | 129 ++++++++++ src/packaging/__init__.py | 15 ++ src/packaging/pyinstaller_hooks.py | 279 ++++++++++++++++++++ src/packaging/resource_bundler.py | 297 +++++++++++++++++++++ test_config_system.py | 216 ++++++++++++++++ test_packaging.py | 305 ++++++++++++++++++++++ 21 files changed, 3514 insertions(+), 40 deletions(-) create mode 100644 Makefile create mode 100644 PYINSTALLER_GUIDE.md create mode 100644 build/linux/hello-scan-code.spec create mode 100644 build/windows/hello-scan-code.spec create mode 100644 config/config.template.json create mode 100644 config/default.json create mode 100644 config/example.json create mode 100644 scripts/build_linux.py create mode 100644 scripts/build_windows.py create mode 100644 src/config/__init__.py create mode 100644 src/config/config_loader.py create mode 100644 src/config/config_validator.py create mode 100644 src/config/default_config.py create mode 100644 src/packaging/__init__.py create mode 100644 src/packaging/pyinstaller_hooks.py create mode 100644 src/packaging/resource_bundler.py create mode 100644 test_config_system.py create mode 100644 test_packaging.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2cffbf3 --- /dev/null +++ b/Makefile @@ -0,0 +1,132 @@ +# Hello-Scan-Code PyInstaller 构建 Makefile + +.PHONY: help clean test install build-windows build-linux package all + +# 默认目标 +help: + @echo "Hello-Scan-Code PyInstaller 构建工具" + @echo "" + @echo "可用目标:" + @echo " help - 显示此帮助信息" + @echo " install - 安装项目依赖" + @echo " test - 运行所有测试" + @echo " test-config - 测试配置系统" + @echo " test-packaging - 测试打包功能" + @echo " clean - 清理构建目录" + @echo " build-windows - 构建Windows可执行文件" + @echo " build-linux - 构建Linux可执行文件" + @echo " package - 创建分发包" + @echo " all - 执行完整构建流程" + @echo "" + @echo "使用示例:" + @echo " make install # 安装依赖" + @echo " make test # 运行测试" + @echo " make build-windows # 构建Windows版本" + @echo " make clean # 清理构建目录" + +# 安装项目依赖 +install: + @echo "安装项目依赖..." + pip install --upgrade pip + pip install pyinstaller>=6.0.0 + pip install loguru pandas openpyxl sqlalchemy alembic + +# 运行所有测试 +test: test-config test-packaging + +# 测试配置系统 +test-config: + @echo "测试配置系统..." + python test_config_system.py + +# 测试打包功能 +test-packaging: + @echo "测试打包功能..." + python test_packaging.py + +# 清理构建目录 +clean: + @echo "清理构建目录..." + rm -rf dist/ + rm -rf build/__pycache__/ + find . -name "*.pyc" -delete + find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true + +# 构建Windows可执行文件 +build-windows: + @echo "构建Windows可执行文件..." + python scripts/build_windows.py + +# 构建Linux可执行文件 +build-linux: + @echo "构建Linux可执行文件..." + python scripts/build_linux.py + +# 创建分发包 +package: + @echo "创建分发包..." + @if [ -d "dist/windows" ]; then \ + cd dist && zip -r hello-scan-code-windows.zip windows/; \ + echo "Windows分发包: dist/hello-scan-code-windows.zip"; \ + fi + @if [ -d "dist/linux" ]; then \ + cd dist && tar -czf hello-scan-code-linux.tar.gz linux/; \ + echo "Linux分发包: dist/hello-scan-code-linux.tar.gz"; \ + fi + +# 执行完整构建流程 +all: clean install test + @echo "执行完整构建流程..." + @echo "当前平台: $$(uname -s)" + @if [ "$$(uname -s)" = "Linux" ]; then \ + $(MAKE) build-linux; \ + elif [ "$$(uname -s)" = "Darwin" ]; then \ + $(MAKE) build-linux; \ + else \ + $(MAKE) build-windows; \ + fi + $(MAKE) package + @echo "构建完成!" + +# 快速构建(跳过测试) +quick-build: + @echo "快速构建..." + @if [ "$$(uname -s)" = "Linux" ]; then \ + $(MAKE) build-linux; \ + else \ + $(MAKE) build-windows; \ + fi + +# 开发模式安装 +dev-install: install + @echo "安装开发依赖..." + pip install pytest black flake8 mypy + +# 代码格式化 +format: + @echo "格式化代码..." + black src/ scripts/ *.py + +# 代码检查 +lint: + @echo "代码检查..." + flake8 src/ scripts/ *.py + mypy src/ --ignore-missing-imports + +# 创建配置文件 +config: + @echo "创建配置文件..." + @if [ ! -f "config.json" ]; then \ + cp config/config.template.json config.json; \ + echo "已创建 config.json,请根据需要修改配置"; \ + else \ + echo "config.json 已存在"; \ + fi + +# 显示构建信息 +info: + @echo "构建环境信息:" + @echo "Python版本: $$(python --version 2>&1)" + @echo "操作系统: $$(uname -s)" + @echo "项目目录: $$(pwd)" + @echo "配置文件: $$([ -f config.json ] && echo '存在' || echo '不存在')" \ No newline at end of file diff --git a/PYINSTALLER_GUIDE.md b/PYINSTALLER_GUIDE.md new file mode 100644 index 0000000..65589ef --- /dev/null +++ b/PYINSTALLER_GUIDE.md @@ -0,0 +1,399 @@ +# PyInstaller 打包使用指南 + +## 概述 + +Hello-Scan-Code 现已支持 PyInstaller 打包,可以生成独立的可执行文件,无需在目标系统安装 Python 环境即可运行。 + +## 新功能特性 + +### JSON 配置系统 +- **外置配置文件**:支持 `config.json` 配置文件,实现配置与代码分离 +- **配置验证**:自动验证配置文件格式和数据有效性 +- **向后兼容**:保持与现有 Python 配置方式的兼容性 +- **默认回退**:配置文件缺失或无效时自动使用默认配置 + +### 跨平台打包 +- **Windows 支持**:生成单文件可执行程序 (.exe) +- **Linux 支持**:生成目录式应用包 +- **自动化构建**:提供构建脚本简化打包流程 +- **资源管理**:自动打包配置文件、数据库迁移等资源 + +## 目录结构 + +``` +项目根目录/ +├── src/ +│ ├── config/ # JSON配置系统 +│ │ ├── config_loader.py # 配置加载器 +│ │ ├── config_validator.py # 配置验证器 +│ │ └── default_config.py # 默认配置定义 +│ ├── packaging/ # 打包支持模块 +│ │ ├── pyinstaller_hooks.py # PyInstaller钩子 +│ │ └── resource_bundler.py # 资源打包器 +│ └── ... (现有模块) +├── config/ # 配置文件目录 +│ ├── config.template.json # 配置模板 +│ └── default.json # 默认配置 +├── build/ # 构建配置 +│ ├── windows/ +│ │ └── hello-scan-code.spec # Windows打包配置 +│ └── linux/ +│ └── hello-scan-code.spec # Linux打包配置 +├── scripts/ # 构建脚本 +│ ├── build_windows.py # Windows构建脚本 +│ └── build_linux.py # Linux构建脚本 +└── dist/ # 打包输出目录 +``` + +## 快速开始 + +### 1. 安装依赖 + +```bash +# 安装项目依赖(包括PyInstaller) +pip install -r requirements.txt + +# 或者手动安装 +pip install pyinstaller>=6.0.0 loguru pandas openpyxl sqlalchemy alembic +``` + +### 2. 配置文件设置 + +#### 创建配置文件 +```bash +# 复制配置模板 +cp config/config.template.json config.json + +# 编辑配置文件 +vim config.json +``` + +#### 配置文件示例 +```json +{ + "repo_path": ".", + "search_term": "test,def,void", + "is_regex": false, + "validate": false, + "validate_workers": 4, + "output": { + "db_path": "results.db", + "excel_path": "results.xlsx" + }, + "logging": { + "level": "INFO" + }, + "filters": { + "ignore_dirs": [ + ".git", "__pycache__", ".svn", ".hg", + ".idea", ".vscode", "node_modules", ".tox" + ], + "file_extensions": null + } +} +``` + +### 3. 构建可执行文件 + +#### Windows 平台 +```bash +# 运行Windows构建脚本 +python scripts/build_windows.py + +# 带依赖安装的构建 +python scripts/build_windows.py --install-deps + +# 不清理构建目录 +python scripts/build_windows.py --no-clean +``` + +#### Linux 平台 +```bash +# 运行Linux构建脚本 +python scripts/build_linux.py + +# 带依赖安装的构建 +python scripts/build_linux.py --install-deps + +# 不清理构建目录 +python scripts/build_linux.py --no-clean +``` + +### 4. 测试打包功能 + +```bash +# 测试配置系统 +python test_config_system.py + +# 测试打包功能 +python test_packaging.py +``` + +## 配置系统详解 + +### 配置加载优先级 + +1. **JSON配置文件**:优先加载可执行文件同目录下的 `config.json` +2. **默认配置**:如果JSON配置不存在或无效,使用内置默认配置 +3. **配置合并**:用户配置与默认配置进行智能合并 + +### 配置文件位置 + +- **开发环境**:项目根目录下的 `config.json` +- **打包后**:可执行文件同目录下的 `config.json` +- **模板文件**:始终包含 `config.template.json` 作为参考 + +### 配置验证 + +系统会自动验证配置文件的: +- **JSON格式正确性** +- **字段类型有效性** +- **数值范围合理性** +- **路径存在性** + +### 错误处理 + +- **格式错误**:显示详细的JSON解析错误信息 +- **验证失败**:列出所有验证错误项 +- **自动回退**:错误时自动使用默认配置并记录警告 +- **路径创建**:自动创建输出文件的父目录 + +## 构建脚本使用 + +### Windows 构建脚本 (`scripts/build_windows.py`) + +```bash +# 基本构建 +python scripts/build_windows.py + +# 显示帮助 +python scripts/build_windows.py --help + +# 可选参数 +--no-clean # 不清理构建目录 +--install-deps # 安装构建依赖 +--project-root # 指定项目根目录 +``` + +**输出文件**:`dist/windows/hello-scan-code.exe` + +### Linux 构建脚本 (`scripts/build_linux.py`) + +```bash +# 基本构建 +python scripts/build_linux.py + +# 显示帮助 +python scripts/build_linux.py --help + +# 可选参数 +--no-clean # 不清理构建目录 +--install-deps # 安装构建依赖 +--project-root # 指定项目根目录 +``` + +**输出文件**:`dist/linux/hello-scan-code/` 目录 + +## 手动打包 + +如果需要手动执行PyInstaller打包: + +### Windows 单文件模式 +```bash +cd 项目根目录 +python -m PyInstaller --clean --noconfirm build/windows/hello-scan-code.spec +``` + +### Linux 目录模式 +```bash +cd 项目根目录 +python -m PyInstaller --clean --noconfirm build/linux/hello-scan-code.spec +``` + +## 部署指南 + +### Windows 部署 + +1. **下载分发包**:`hello-scan-code-v1.0.0-windows.zip` +2. **解压到目标目录** +3. **配置文件**: + ```bash + # 复制配置模板 + copy config.template.json config.json + + # 编辑配置文件 + notepad config.json + ``` +4. **运行程序**: + ```bash + hello-scan-code.exe + ``` + +### Linux 部署 + +1. **下载分发包**:`hello-scan-code-v1.0.0-linux.tar.gz` +2. **解压到目标目录**: + ```bash + tar -xzf hello-scan-code-v1.0.0-linux.tar.gz + cd hello-scan-code-v1.0.0-linux + ``` +3. **配置文件**: + ```bash + # 复制配置模板 + cp hello-scan-code/config.template.json config.json + + # 编辑配置文件 + vim config.json + ``` +4. **运行程序**: + ```bash + # 使用启动脚本(推荐) + ./run-hello-scan-code.sh + + # 或直接运行 + ./hello-scan-code/hello-scan-code + ``` + +## 配置示例 + +### 基本搜索配置 +```json +{ + "repo_path": "/path/to/your/code", + "search_term": "function,class,def", + "is_regex": false, + "validate": false, + "output": { + "db_path": "search_results.db", + "excel_path": "search_report.xlsx" + } +} +``` + +### 正则表达式搜索 +```json +{ + "repo_path": ".", + "search_term": "def\\s+\\w+\\s*\\(", + "is_regex": true, + "validate": true, + "validate_workers": 8, + "filters": { + "file_extensions": [".py", ".js", ".java"] + } +} +``` + +### 性能优化配置 +```json +{ + "repo_path": ".", + "search_term": "TODO,FIXME,BUG", + "validate": true, + "validate_workers": 16, + "filters": { + "ignore_dirs": [ + ".git", "node_modules", "dist", "build", + "__pycache__", ".vscode", ".idea" + ] + }, + "logging": { + "level": "DEBUG" + } +} +``` + +## 故障排除 + +### 常见问题 + +1. **PyInstaller 安装失败** + ```bash + # 升级pip + pip install --upgrade pip + + # 清理缓存后重新安装 + pip cache purge + pip install pyinstaller + ``` + +2. **打包时缺少模块** + - 检查 `src/packaging/pyinstaller_hooks.py` 中的隐藏导入列表 + - 添加缺失的模块到 `hiddenimports` 列表 + +3. **配置文件加载失败** + - 验证JSON格式正确性 + - 检查文件编码为UTF-8 + - 确认路径使用正斜杠或转义反斜杠 + +4. **可执行文件过大** + - 检查 `excludes` 列表,排除不需要的模块 + - 考虑使用目录模式而非单文件模式 + +5. **运行时找不到资源文件** + - 检查 `src/packaging/resource_bundler.py` 中的资源文件收集 + - 确认资源文件已正确添加到 `datas` 列表 + +### 调试技巧 + +1. **启用详细日志** + ```json + { + "logging": { + "level": "DEBUG" + } + } + ``` + +2. **使用控制台模式** + - Windows: 确保 `.spec` 文件中 `console=True` + - 查看详细的错误信息和日志输出 + +3. **验证打包结果** + ```bash + # 运行测试脚本 + python test_packaging.py + + # 检查资源文件 + python -c "from src.packaging.resource_bundler import ResourceBundler; ResourceBundler().validate_resources()" + ``` + +## 开发者指南 + +### 添加新的配置项 + +1. **更新默认配置**:修改 `src/config/default_config.py` +2. **更新验证器**:在 `src/config/config_validator.py` 中添加验证规则 +3. **更新模板**:修改 `config/config.template.json` + +### 添加新的依赖 + +1. **更新打包钩子**:在 `src/packaging/pyinstaller_hooks.py` 中添加隐藏导入 +2. **测试打包**:确保新依赖正确打包 +3. **更新文档**:在本文档中说明新依赖的作用 + +### 自定义构建流程 + +1. **修改 .spec 文件**:调整打包参数 +2. **扩展构建脚本**:添加自定义构建步骤 +3. **更新资源收集**:修改 `resource_bundler.py` 收集新资源 + +## 版本更新 + +当更新版本时: + +1. **更新版本号**:在构建脚本中修改版本号 +2. **更新配置**:如有配置格式变更,提供迁移指南 +3. **测试兼容性**:确保向后兼容性 +4. **更新文档**:同步更新使用文档 + +## 技术支持 + +如遇到问题,请检查: + +1. **Python版本**:确保使用 Python 3.10+ +2. **依赖版本**:确保所有依赖包版本符合要求 +3. **系统环境**:确认目标系统的兼容性 +4. **配置文件**:验证配置文件格式和内容正确性 + +更多技术支持请参考项目 README 或提交 Issue。 \ No newline at end of file diff --git a/README.md b/README.md index 206e58a..fd716f6 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,172 @@ # Hello-Scan-Code - 高效代码搜索工具 -Hello-Scan-Code 是一个专为大型代码仓库设计的高效搜索工具。它结合了 `grep` 的速度和 Python 的灵活性,能够快速定位包含特定字符串或正则表达式的文件。 +Hello-Scan-Code 是一个专为大型代码仓库设计的高效搜索工具。它结合了 `grep` 的速度和 Python 的灵活性,能够快速定位包含特定字符串或正则表达式的文件。现已支持PyInstaller打包,可生成独立可执行文件。 -## 功能特性 +## ✨ 新功能特性 -- **高速搜索**:优先使用系统 `grep` 命令,自动降级到 Python 实现 -- **多关键字搜索**:支持同时搜索多个关键字,用逗号分隔 -- **灵活过滤**:支持配置忽略目录和指定文件后缀 -- **多格式输出**:支持 SQLite 数据库和 Excel 文件输出 -- **详细日志**:集成 `loguru` 日志库,记录详细的运行信息 +### 🚀 PyInstaller 打包支持 +- **独立部署**:生成无需Python环境的可执行文件 +- **跨平台分发**:支持Windows和Linux平台 +- **零依赖运行**:打包后程序包含所有必要依赖 + +### 📋 JSON 配置系统 +- **外置配置**:支持 `config.json` 配置文件 +- **配置验证**:自动验证配置文件格式和有效性 +- **向后兼容**:保持与现有配置方式的兼容性 +- **智能回退**:配置错误时自动使用默认配置 + +## 📦 安装方式 + +### 方式一:下载可执行文件(推荐) + +从 [Releases](https://github.com/your-repo/releases) 页面下载对应平台的预编译版本: + +- `hello-scan-code-v1.0.0-windows.zip` - Windows 平台 +- `hello-scan-code-v1.0.0-linux.tar.gz` - Linux 平台 + +### 方式二:从源码安装(开发者) + +```bash +# 克隆项目 +git clone +cd Hello-Scan-Code + +# 安装依赖 +pip install -r requirements.txt + +# 或使用Makefile +make install +``` + +## 🚀 快速开始 + +### 使用可执行文件 + +#### Windows +```bash +# 解压下载的文件 +unzip hello-scan-code-v1.0.0-windows.zip +cd hello-scan-code-v1.0.0-windows + +# 创建配置文件 +copy config.template.json config.json + +# 编辑配置文件 +notepad config.json + +# 运行程序 +hello-scan-code.exe +``` + +#### Linux +```bash +# 解压下载的文件 +tar -xzf hello-scan-code-v1.0.0-linux.tar.gz +cd hello-scan-code-v1.0.0-linux + +# 使用启动脚本(推荐) +./run-hello-scan-code.sh + +# 或直接运行 +./hello-scan-code/hello-scan-code +``` + +### 从源码运行 + +```bash +# 创建配置文件 +cp config/config.template.json config.json + +# 编辑配置文件 +vim config.json + +# 运行程序 +python main.py +``` + +## 📋 JSON 配置系统 + +### 配置文件示例 + +```json +{ + "repo_path": ".", + "search_term": "TODO,FIXME,BUG", + "is_regex": false, + "validate": true, + "validate_workers": 4, + + "output": { + "db_path": "results.db", + "excel_path": "results.xlsx" + }, + + "logging": { + "level": "INFO" + }, + + "filters": { + "ignore_dirs": [ + ".git", "__pycache__", "node_modules", + "dist", "build", ".vscode", ".idea" + ], + "file_extensions": [".py", ".js", ".java"] + } +} +``` + +### 配置参数说明 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `repo_path` | string | `"."` | 搜索目标代码仓库路径 | +| `search_term` | string | `"test,def,void"` | 搜索关键词,逗号分隔 | +| `is_regex` | boolean | `false` | 是否启用正则表达式搜索 | +| `validate` | boolean | `false` | 是否启用结果二次验证 | +| `validate_workers` | integer | `4` | 验证工作线程数量 | +| `output.db_path` | string | `"results.db"` | SQLite数据库输出路径 | +| `output.excel_path` | string | `"results.xlsx"` | Excel报告输出路径 | +| `logging.level` | string | `"INFO"` | 日志级别 | +| `filters.ignore_dirs` | array | `[".git", "__pycache__"]` | 忽略目录列表 | +| `filters.file_extensions` | array\|null | `null` | 文件扩展名过滤器 | + +## 🔧 开发者指南 + +### 构建可执行文件 + +```bash +# 使用Makefile构建 +make build-windows # Windows平台 +make build-linux # Linux平台 +make all # 完整构建流程 + +# 或手动执行构建脚本 +python scripts/build_windows.py +python scripts/build_linux.py +``` + +### 测试功能 + +```bash +# 测试配置系统 +python test_config_system.py + +# 测试打包功能 +python test_packaging.py + +# 或使用Makefile +make test +``` + +### 手动PyInstaller打包 + +```bash +# Windows单文件模式 +python -m PyInstaller --clean --noconfirm build/windows/hello-scan-code.spec + +# Linux目录模式 +python -m PyInstaller --clean --noconfirm build/linux/hello-scan-code.spec +``` ## 项目结构 @@ -95,9 +253,46 @@ config.file_extensions = [".py", ".js", ".go"] # 文件类型 | `db_path` | SQLite 输出路径 | `db/results.db` | | `excel_path` | Excel 输出路径 | `report/results.xlsx` | -## 技术特点 +## 🔗 相关文档 + +- [PyInstaller打包使用指南](PYINSTALLER_GUIDE.md) - 详细的打包和部署指南 +- [API文档](docs/api.md) - 开发者API参考 +- [FAQ](docs/faq.md) - 常见问题解答 + +## 📝 更新日志 + +### v1.0.0 (2025-01) +- ✨ 新增 PyInstaller 打包支持 +- ✨ 新增 JSON 配置系统 +- ✨ 新增 配置验证和错误处理 +- ✨ 新增 跨平台构建脚本 +- ✨ 新增 Makefile 构建工具 +- 📝 完善文档和使用指南 + +## 🤝 贡献 + +欢迎提交 Pull Requests 和 Issues! + +### 开发环境设置 + +```bash +# 克隆项目 +git clone +cd Hello-Scan-Code + +# 安装开发依赖 +make dev-install + +# 运行测试 +make test + +# 代码格式化 +make format + +# 代码检查 +make lint +``` + +## 📝 许可证 -- **面向对象设计**:采用策略模式、工厂模式等设计模式 -- **高效搜索**:优先使用 `grep`,自动降级到 Python 实现 -- **智能编码**:自动处理多种文件编码格式 -- **结果导出**:支持 SQLite 和 Excel 双重输出 \ No newline at end of file +本项目采用 MIT 许可证。详情请见 [LICENSE](LICENSE) 文件。 \ No newline at end of file diff --git a/build/linux/hello-scan-code.spec b/build/linux/hello-scan-code.spec new file mode 100644 index 0000000..cc7dd4b --- /dev/null +++ b/build/linux/hello-scan-code.spec @@ -0,0 +1,150 @@ +# -*- mode: python ; coding: utf-8 -*- +""" +Linux平台PyInstaller打包配置文件 +""" + +import sys +import os +from pathlib import Path + +# 添加项目根目录到Python路径 +project_root = Path(__file__).parent.parent.parent +sys.path.insert(0, str(project_root / "src")) + +# 导入打包配置 +from packaging.pyinstaller_hooks import ( + get_analysis_options, + get_exe_options, + get_data_files +) +from packaging.resource_bundler import bundle_resources + +# 配置项目路径 +project_path = str(project_root) +main_script = str(project_root / "main.py") + +# 获取数据文件 +datas = bundle_resources(project_path) + +# 隐藏导入模块 +hiddenimports = [ + # SQLAlchemy相关 + 'sqlalchemy', + 'sqlalchemy.ext.declarative', + 'sqlalchemy.orm', + 'sqlalchemy.sql', + 'sqlalchemy.dialects.sqlite', + 'sqlalchemy.pool', + 'sqlalchemy.engine', + + # Alembic相关 + 'alembic', + 'alembic.runtime.migration', + 'alembic.operations', + 'alembic.script', + + # pandas和Excel导出相关 + 'pandas', + 'pandas.io.excel', + 'openpyxl', + 'openpyxl.workbook', + 'openpyxl.worksheet', + 'openpyxl.styles', + + # loguru日志相关 + 'loguru', + + # 项目模块 + 'src', + 'src.config', + 'src.config.config_loader', + 'src.config.config_validator', + 'src.config.default_config', + 'src.packaging', + 'src.code_searcher', + 'src.database', + 'src.search_template', + 'src.strategies', + 'src.validators', + 'src.exporter', +] + +# 排除的模块 +excludes = [ + 'pytest', + 'unittest', + 'test', + 'tests', + 'pip', + 'setuptools', + 'wheel', + 'distutils', + 'pdb', + 'ipdb', + 'debugpy', + 'sphinx', + 'docutils', + 'jupyter', + 'ipython', + 'notebook', + 'tkinter', + 'matplotlib', + 'pyplot', + 'requests', + 'urllib3', + 'http', + 'email', + 'numpy', + 'scipy', + 'sklearn', +] + +# Analysis配置 +a = Analysis( + [main_script], + pathex=[project_path], + binaries=[], + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=excludes, + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=None, + noarchive=False, +) + +# PYZ配置 +pyz = PYZ(a.pure, a.zipped_data, cipher=None) + +# EXE配置 - Linux目录模式 +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='hello-scan-code', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) + +# COLLECT配置 - 收集所有文件到目录 +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='hello-scan-code', +) \ No newline at end of file diff --git a/build/windows/hello-scan-code.spec b/build/windows/hello-scan-code.spec new file mode 100644 index 0000000..323d248 --- /dev/null +++ b/build/windows/hello-scan-code.spec @@ -0,0 +1,143 @@ +# -*- mode: python ; coding: utf-8 -*- +""" +Windows平台PyInstaller打包配置文件 +""" + +import sys +import os +from pathlib import Path + +# 添加项目根目录到Python路径 +project_root = Path(__file__).parent.parent.parent +sys.path.insert(0, str(project_root / "src")) + +# 导入打包配置 +from packaging.pyinstaller_hooks import ( + get_analysis_options, + get_exe_options, + get_data_files +) +from packaging.resource_bundler import bundle_resources + +# 配置项目路径 +project_path = str(project_root) +main_script = str(project_root / "main.py") + +# 获取数据文件 +datas = bundle_resources(project_path) + +# 隐藏导入模块 +hiddenimports = [ + # SQLAlchemy相关 + 'sqlalchemy', + 'sqlalchemy.ext.declarative', + 'sqlalchemy.orm', + 'sqlalchemy.sql', + 'sqlalchemy.dialects.sqlite', + 'sqlalchemy.pool', + 'sqlalchemy.engine', + + # Alembic相关 + 'alembic', + 'alembic.runtime.migration', + 'alembic.operations', + 'alembic.script', + + # pandas和Excel导出相关 + 'pandas', + 'pandas.io.excel', + 'openpyxl', + 'openpyxl.workbook', + 'openpyxl.worksheet', + 'openpyxl.styles', + + # loguru日志相关 + 'loguru', + + # 项目模块 + 'src', + 'src.config', + 'src.config.config_loader', + 'src.config.config_validator', + 'src.config.default_config', + 'src.packaging', + 'src.code_searcher', + 'src.database', + 'src.search_template', + 'src.strategies', + 'src.validators', + 'src.exporter', +] + +# 排除的模块 +excludes = [ + 'pytest', + 'unittest', + 'test', + 'tests', + 'pip', + 'setuptools', + 'wheel', + 'distutils', + 'pdb', + 'ipdb', + 'debugpy', + 'sphinx', + 'docutils', + 'jupyter', + 'ipython', + 'notebook', + 'tkinter', + 'matplotlib', + 'pyplot', + 'requests', + 'urllib3', + 'http', + 'email', + 'numpy', + 'scipy', + 'sklearn', +] + +# Analysis配置 +a = Analysis( + [main_script], + pathex=[project_path], + binaries=[], + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=excludes, + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=None, + noarchive=False, +) + +# PYZ配置 +pyz = PYZ(a.pure, a.zipped_data, cipher=None) + +# EXE配置 - Windows单文件模式 +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='hello-scan-code', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=None, # 可以在这里添加图标文件路径 +) \ No newline at end of file diff --git a/config/config.template.json b/config/config.template.json new file mode 100644 index 0000000..80a2fae --- /dev/null +++ b/config/config.template.json @@ -0,0 +1,35 @@ +{ + "// 注释": "Hello-Scan-Code 配置文件模板", + "// 说明": "复制此文件为 config.json 并修改相应配置项", + + "repo_path": ".", + "search_term": "test,def,void", + "is_regex": false, + "validate": false, + "validate_workers": 4, + + "output": { + "db_path": "results.db", + "excel_path": "results.xlsx" + }, + + "logging": { + "level": "INFO" + }, + + "filters": { + "ignore_dirs": [ + ".git", + "__pycache__", + ".svn", + ".hg", + ".idea", + ".vscode", + "node_modules", + ".tox", + "dist", + "build" + ], + "file_extensions": null + } +} \ No newline at end of file diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..ab16f0a --- /dev/null +++ b/config/default.json @@ -0,0 +1,32 @@ +{ + "repo_path": ".", + "search_term": "test,def,void", + "is_regex": false, + "validate": false, + "validate_workers": 4, + + "output": { + "db_path": "results.db", + "excel_path": "results.xlsx" + }, + + "logging": { + "level": "INFO" + }, + + "filters": { + "ignore_dirs": [ + ".git", + "__pycache__", + ".svn", + ".hg", + ".idea", + ".vscode", + "node_modules", + ".tox", + "dist", + "build" + ], + "file_extensions": null + } +} \ No newline at end of file diff --git a/config/example.json b/config/example.json new file mode 100644 index 0000000..763e7b1 --- /dev/null +++ b/config/example.json @@ -0,0 +1,42 @@ +{ + "_comment": "Hello-Scan-Code 示例配置文件", + "_description": "此文件展示了各种配置选项的使用方法", + + "repo_path": ".", + "search_term": "TODO,FIXME,BUG,HACK", + "is_regex": false, + "validate": true, + "validate_workers": 4, + + "output": { + "db_path": "code_analysis.db", + "excel_path": "code_analysis_report.xlsx" + }, + + "logging": { + "level": "INFO" + }, + + "filters": { + "ignore_dirs": [ + ".git", + "__pycache__", + "node_modules", + "dist", + "build", + ".vscode", + ".idea", + ".pytest_cache", + ".tox" + ], + "file_extensions": [ + ".py", + ".js", + ".ts", + ".java", + ".cpp", + ".c", + ".h" + ] + } +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9256936..a5ca21a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,4 +9,5 @@ dependencies = [ "openpyxl>=3.0.0", "sqlalchemy>=2.0.0", "alembic>=1.8.0", + "pyinstaller>=6.0.0", ] \ No newline at end of file diff --git a/scripts/build_linux.py b/scripts/build_linux.py new file mode 100644 index 0000000..6c4b9e9 --- /dev/null +++ b/scripts/build_linux.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +""" +Linux平台构建脚本 + +自动化执行PyInstaller打包流程 +""" + +import os +import sys +import shutil +import subprocess +import platform +from pathlib import Path +import argparse +import logging + +# 设置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class LinuxBuilder: + """Linux平台构建器""" + + def __init__(self, project_root: str = None): + """ + 初始化构建器 + + Args: + project_root: 项目根目录 + """ + if project_root is None: + self.project_root = Path(__file__).parent.parent + else: + self.project_root = Path(project_root) + + self.build_dir = self.project_root / "build" / "linux" + self.dist_dir = self.project_root / "dist" / "linux" + self.spec_file = self.build_dir / "hello-scan-code.spec" + + def check_requirements(self) -> bool: + """ + 检查构建要求 + + Returns: + bool: 是否满足构建要求 + """ + logger.info("检查构建要求...") + + # 检查操作系统 + if platform.system() != "Linux": + logger.warning("当前不是Linux系统,但仍可尝试构建") + + # 检查Python版本 + python_version = sys.version_info + if python_version < (3, 10): + logger.error(f"Python版本过低: {python_version}, 需要3.10+") + return False + + # 检查PyInstaller + try: + import PyInstaller + logger.info(f"PyInstaller版本: {PyInstaller.__version__}") + except ImportError: + logger.error("PyInstaller未安装,请执行: pip install pyinstaller") + return False + + # 检查项目依赖 + required_packages = ['loguru', 'pandas', 'openpyxl', 'sqlalchemy', 'alembic'] + missing_packages = [] + + for package in required_packages: + try: + __import__(package) + except ImportError: + missing_packages.append(package) + + if missing_packages: + logger.error(f"缺少依赖包: {', '.join(missing_packages)}") + logger.error("请执行: pip install -r requirements.txt") + return False + + # 检查spec文件 + if not self.spec_file.exists(): + logger.error(f"spec文件不存在: {self.spec_file}") + return False + + logger.info("所有构建要求已满足") + return True + + def clean_build(self): + """清理构建目录""" + logger.info("清理构建目录...") + + clean_dirs = [ + self.dist_dir, + self.project_root / "build" / "__pycache__", + self.project_root / "dist" / "__pycache__", + ] + + for dir_path in clean_dirs: + if dir_path.exists(): + shutil.rmtree(dir_path) + logger.info(f"已清理: {dir_path}") + + def install_dependencies(self): + """安装构建依赖""" + logger.info("安装构建依赖...") + + try: + # 升级PyInstaller到最新版本 + subprocess.run([ + sys.executable, "-m", "pip", "install", "--upgrade", "pyinstaller" + ], check=True) + + logger.info("依赖安装完成") + except subprocess.CalledProcessError as e: + logger.error(f"依赖安装失败: {e}") + raise + + def build_executable(self) -> bool: + """ + 构建可执行文件 + + Returns: + bool: 是否构建成功 + """ + logger.info("开始构建可执行文件...") + + try: + # 切换到项目根目录 + os.chdir(self.project_root) + + # 执行PyInstaller + cmd = [ + sys.executable, "-m", "PyInstaller", + "--clean", + "--noconfirm", + str(self.spec_file) + ] + + logger.info(f"执行命令: {' '.join(cmd)}") + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + logger.info("构建成功!") + logger.info(f"构建输出: {result.stdout}") + return True + else: + logger.error("构建失败!") + logger.error(f"错误输出: {result.stderr}") + return False + + except Exception as e: + logger.error(f"构建过程中出现异常: {e}") + return False + + def post_build_tasks(self): + """构建后任务""" + logger.info("执行构建后任务...") + + # 检查输出目录 + app_dir = self.dist_dir / "hello-scan-code" + exe_file = app_dir / "hello-scan-code" + + if not exe_file.exists(): + logger.error("可执行文件未生成") + return False + + # 设置可执行权限 + exe_file.chmod(0o755) + logger.info("已设置可执行权限") + + # 计算目录大小 + total_size = sum(f.stat().st_size for f in app_dir.rglob('*') if f.is_file()) + size_mb = total_size / (1024 * 1024) + logger.info(f"应用程序目录大小: {size_mb:.2f} MB") + + # 复制配置模板到应用目录 + template_file = self.project_root / "config" / "config.template.json" + if template_file.exists(): + shutil.copy2(template_file, app_dir / "config.template.json") + logger.info("已复制配置模板文件") + + # 复制README和LICENSE到分发目录根部 + for doc_file in ["README.md", "LICENSE"]: + src_file = self.project_root / doc_file + if src_file.exists(): + shutil.copy2(src_file, self.dist_dir / doc_file) + logger.info(f"已复制: {doc_file}") + + # 创建启动脚本 + self.create_launcher_script() + + return True + + def create_launcher_script(self): + """创建启动脚本""" + launcher_content = '''#!/bin/bash +# Hello-Scan-Code 启动脚本 + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="$SCRIPT_DIR/hello-scan-code" +EXECUTABLE="$APP_DIR/hello-scan-code" + +# 检查可执行文件是否存在 +if [ ! -f "$EXECUTABLE" ]; then + echo "错误: 可执行文件不存在: $EXECUTABLE" + exit 1 +fi + +# 检查配置模板 +TEMPLATE_FILE="$APP_DIR/config.template.json" +CONFIG_FILE="$SCRIPT_DIR/config.json" + +if [ ! -f "$CONFIG_FILE" ] && [ -f "$TEMPLATE_FILE" ]; then + echo "提示: 首次运行,已为您复制配置模板文件" + echo "配置文件位置: $CONFIG_FILE" + echo "请根据需要修改配置文件,然后重新运行此脚本" + cp "$TEMPLATE_FILE" "$CONFIG_FILE" + exit 0 +fi + +# 切换到脚本目录(确保相对路径正确) +cd "$SCRIPT_DIR" + +# 运行应用程序 +echo "启动 Hello-Scan-Code..." +"$EXECUTABLE" "$@" +''' + + launcher_path = self.dist_dir / "run-hello-scan-code.sh" + with open(launcher_path, 'w', encoding='utf-8') as f: + f.write(launcher_content) + + # 设置可执行权限 + launcher_path.chmod(0o755) + logger.info(f"已创建启动脚本: {launcher_path}") + + def create_distribution_package(self): + """创建分发包""" + logger.info("创建分发包...") + + try: + # 创建版本号 + version = "1.0.0" # 可以从git或配置文件读取 + + # 压缩包名称 + package_name = f"hello-scan-code-v{version}-linux" + package_path = self.project_root / "dist" / f"{package_name}.tar.gz" + + # 创建tar.gz压缩包 + shutil.make_archive( + str(package_path.with_suffix('').with_suffix('')), + 'gztar', + self.dist_dir.parent, + 'linux' + ) + + logger.info(f"分发包已创建: {package_path}") + + except Exception as e: + logger.error(f"创建分发包失败: {e}") + + def run_build(self, clean: bool = True, install_deps: bool = False) -> bool: + """ + 运行完整构建流程 + + Args: + clean: 是否清理构建目录 + install_deps: 是否安装依赖 + + Returns: + bool: 是否构建成功 + """ + logger.info("开始Linux平台构建...") + + try: + # 检查要求 + if not self.check_requirements(): + return False + + # 清理构建目录 + if clean: + self.clean_build() + + # 安装依赖 + if install_deps: + self.install_dependencies() + + # 构建可执行文件 + if not self.build_executable(): + return False + + # 构建后任务 + if not self.post_build_tasks(): + return False + + # 创建分发包 + self.create_distribution_package() + + logger.info("Linux构建完成!") + return True + + except Exception as e: + logger.error(f"构建失败: {e}") + return False + + +def main(): + """主函数""" + parser = argparse.ArgumentParser(description="Linux平台构建脚本") + parser.add_argument("--no-clean", action="store_true", help="不清理构建目录") + parser.add_argument("--install-deps", action="store_true", help="安装构建依赖") + parser.add_argument("--project-root", help="项目根目录路径") + + args = parser.parse_args() + + builder = LinuxBuilder(args.project_root) + success = builder.run_build( + clean=not args.no_clean, + install_deps=args.install_deps + ) + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/build_windows.py b/scripts/build_windows.py new file mode 100644 index 0000000..d944f10 --- /dev/null +++ b/scripts/build_windows.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +""" +Windows平台构建脚本 + +自动化执行PyInstaller打包流程 +""" + +import os +import sys +import shutil +import subprocess +import platform +from pathlib import Path +import argparse +import logging + +# 设置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class WindowsBuilder: + """Windows平台构建器""" + + def __init__(self, project_root: str = None): + """ + 初始化构建器 + + Args: + project_root: 项目根目录 + """ + if project_root is None: + self.project_root = Path(__file__).parent.parent + else: + self.project_root = Path(project_root) + + self.build_dir = self.project_root / "build" / "windows" + self.dist_dir = self.project_root / "dist" / "windows" + self.spec_file = self.build_dir / "hello-scan-code.spec" + + def check_requirements(self) -> bool: + """ + 检查构建要求 + + Returns: + bool: 是否满足构建要求 + """ + logger.info("检查构建要求...") + + # 检查操作系统 + if platform.system() != "Windows": + logger.warning("当前不是Windows系统,但仍可尝试构建") + + # 检查Python版本 + python_version = sys.version_info + if python_version < (3, 10): + logger.error(f"Python版本过低: {python_version}, 需要3.10+") + return False + + # 检查PyInstaller + try: + import PyInstaller + logger.info(f"PyInstaller版本: {PyInstaller.__version__}") + except ImportError: + logger.error("PyInstaller未安装,请执行: pip install pyinstaller") + return False + + # 检查项目依赖 + required_packages = ['loguru', 'pandas', 'openpyxl', 'sqlalchemy', 'alembic'] + missing_packages = [] + + for package in required_packages: + try: + __import__(package) + except ImportError: + missing_packages.append(package) + + if missing_packages: + logger.error(f"缺少依赖包: {', '.join(missing_packages)}") + logger.error("请执行: pip install -r requirements.txt") + return False + + # 检查spec文件 + if not self.spec_file.exists(): + logger.error(f"spec文件不存在: {self.spec_file}") + return False + + logger.info("所有构建要求已满足") + return True + + def clean_build(self): + """清理构建目录""" + logger.info("清理构建目录...") + + clean_dirs = [ + self.dist_dir, + self.project_root / "build" / "__pycache__", + self.project_root / "dist" / "__pycache__", + ] + + for dir_path in clean_dirs: + if dir_path.exists(): + shutil.rmtree(dir_path) + logger.info(f"已清理: {dir_path}") + + def install_dependencies(self): + """安装构建依赖""" + logger.info("安装构建依赖...") + + try: + # 升级PyInstaller到最新版本 + subprocess.run([ + sys.executable, "-m", "pip", "install", "--upgrade", "pyinstaller" + ], check=True) + + logger.info("依赖安装完成") + except subprocess.CalledProcessError as e: + logger.error(f"依赖安装失败: {e}") + raise + + def build_executable(self) -> bool: + """ + 构建可执行文件 + + Returns: + bool: 是否构建成功 + """ + logger.info("开始构建可执行文件...") + + try: + # 切换到项目根目录 + os.chdir(self.project_root) + + # 执行PyInstaller + cmd = [ + sys.executable, "-m", "PyInstaller", + "--clean", + "--noconfirm", + str(self.spec_file) + ] + + logger.info(f"执行命令: {' '.join(cmd)}") + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + logger.info("构建成功!") + logger.info(f"构建输出: {result.stdout}") + return True + else: + logger.error("构建失败!") + logger.error(f"错误输出: {result.stderr}") + return False + + except Exception as e: + logger.error(f"构建过程中出现异常: {e}") + return False + + def post_build_tasks(self): + """构建后任务""" + logger.info("执行构建后任务...") + + # 检查输出文件 + exe_file = self.dist_dir / "hello-scan-code.exe" + if exe_file.exists(): + file_size = exe_file.stat().st_size / (1024 * 1024) # MB + logger.info(f"可执行文件大小: {file_size:.2f} MB") + else: + logger.error("可执行文件未生成") + return False + + # 复制配置模板 + template_file = self.project_root / "config" / "config.template.json" + if template_file.exists(): + shutil.copy2(template_file, self.dist_dir / "config.template.json") + logger.info("已复制配置模板文件") + + # 复制README和LICENSE + for doc_file in ["README.md", "LICENSE"]: + src_file = self.project_root / doc_file + if src_file.exists(): + shutil.copy2(src_file, self.dist_dir / doc_file) + logger.info(f"已复制: {doc_file}") + + return True + + def create_distribution_package(self): + """创建分发包""" + logger.info("创建分发包...") + + try: + # 创建版本号 + version = "1.0.0" # 可以从git或配置文件读取 + + # 压缩包名称 + package_name = f"hello-scan-code-v{version}-windows" + package_path = self.project_root / "dist" / f"{package_name}.zip" + + # 创建压缩包 + shutil.make_archive( + str(package_path.with_suffix('')), + 'zip', + self.dist_dir.parent, + 'windows' + ) + + logger.info(f"分发包已创建: {package_path}") + + except Exception as e: + logger.error(f"创建分发包失败: {e}") + + def run_build(self, clean: bool = True, install_deps: bool = False) -> bool: + """ + 运行完整构建流程 + + Args: + clean: 是否清理构建目录 + install_deps: 是否安装依赖 + + Returns: + bool: 是否构建成功 + """ + logger.info("开始Windows平台构建...") + + try: + # 检查要求 + if not self.check_requirements(): + return False + + # 清理构建目录 + if clean: + self.clean_build() + + # 安装依赖 + if install_deps: + self.install_dependencies() + + # 构建可执行文件 + if not self.build_executable(): + return False + + # 构建后任务 + if not self.post_build_tasks(): + return False + + # 创建分发包 + self.create_distribution_package() + + logger.info("Windows构建完成!") + return True + + except Exception as e: + logger.error(f"构建失败: {e}") + return False + + +def main(): + """主函数""" + parser = argparse.ArgumentParser(description="Windows平台构建脚本") + parser.add_argument("--no-clean", action="store_true", help="不清理构建目录") + parser.add_argument("--install-deps", action="store_true", help="安装构建依赖") + parser.add_argument("--project-root", help="项目根目录路径") + + args = parser.parse_args() + + builder = WindowsBuilder(args.project_root) + success = builder.run_build( + clean=not args.no_clean, + install_deps=args.install_deps + ) + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/config.py b/src/config.py index f07a63e..c083b9d 100644 --- a/src/config.py +++ b/src/config.py @@ -1,37 +1,68 @@ import argparse -from dataclasses import dataclass, field -from typing import Optional, List import os +from typing import Optional + +# 向后兼容:重新导出新配置系统的类和函数 +from .config.default_config import SearchConfig +from .config.config_loader import ConfigLoader, load_config_from_file -@dataclass -class SearchConfig: - repo_path: str = "/root/CodeRootPath" # 默认为当前目录 - search_term: str = "test,def,void" # 默认搜索词 - is_regex: bool = False - validate: bool = False - validate_workers: int = 4 - db_path: str = "db/results.db" - excel_path: str = "report/results.xlsx" - log_level: str = "INFO" - # 默认忽略的目录 - ignore_dirs: List[str] = field(default_factory=lambda: [".git", "__pycache__", ".svn", ".hg", ".idea", ".vscode", "node_modules", ".tox"]) - # 默认搜索的文件后缀(None表示不限制) - file_extensions: Optional[List[str]] = None def parse_args() -> SearchConfig: """ - 创建配置对象,使用config文件中的默认值 - 支持通过直接修改config参数来配置 + 创建配置对象,优先加载JSON配置文件,回退到默认配置 + 保持向后兼容性 """ - config = SearchConfig() - - # 确保输出目录存在 - db_dir = os.path.dirname(config.db_path) or "." - excel_dir = os.path.dirname(config.excel_path) or "." + try: + # 尝试从JSON配置文件加载 + loader = ConfigLoader() + config = loader.load_config() + + # 确保输出目录存在 + db_dir = os.path.dirname(config.db_path) or "." + excel_dir = os.path.dirname(config.excel_path) or "." + + if db_dir != ".": + os.makedirs(db_dir, exist_ok=True) + if excel_dir != ".": + os.makedirs(excel_dir, exist_ok=True) + + return config + except Exception as e: + # 如果加载失败,使用默认配置 + print(f"警告: 配置加载失败,使用默认配置: {e}") + config = SearchConfig() + + # 确保输出目录存在 + db_dir = os.path.dirname(config.db_path) or "." + excel_dir = os.path.dirname(config.excel_path) or "." + + if db_dir != ".": + os.makedirs(db_dir, exist_ok=True) + if excel_dir != ".": + os.makedirs(excel_dir, exist_ok=True) + + return config + + +def load_json_config(config_path: Optional[str] = None) -> SearchConfig: + """ + 从JSON文件加载配置 - if db_dir != ".": - os.makedirs(db_dir, exist_ok=True) - if excel_dir != ".": - os.makedirs(excel_dir, exist_ok=True) + Args: + config_path: 配置文件路径,为None时使用默认位置 + + Returns: + SearchConfig: 配置对象 + """ + return load_config_from_file(config_path) + + +def create_config_template(target_dir: Optional[str] = None) -> None: + """ + 创建配置模板文件 - return config \ No newline at end of file + Args: + target_dir: 目标目录,为None时使用当前目录 + """ + from .config.config_loader import create_config_template + create_config_template(target_dir) \ No newline at end of file diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..0201cc7 --- /dev/null +++ b/src/config/__init__.py @@ -0,0 +1,16 @@ +""" +配置系统模块 + +提供JSON配置加载、验证和管理功能 +""" + +from .config_loader import ConfigLoader +from .config_validator import ConfigValidator +from .default_config import DEFAULT_CONFIG, create_default_config + +__all__ = [ + 'ConfigLoader', + 'ConfigValidator', + 'DEFAULT_CONFIG', + 'create_default_config' +] \ No newline at end of file diff --git a/src/config/config_loader.py b/src/config/config_loader.py new file mode 100644 index 0000000..256bd1b --- /dev/null +++ b/src/config/config_loader.py @@ -0,0 +1,216 @@ +""" +配置加载器模块 + +提供JSON配置文件的加载和管理功能 +""" + +import json +import os +import sys +from typing import Dict, Any, Optional +import logging +from pathlib import Path + +from .default_config import DEFAULT_CONFIG, dict_to_search_config, SearchConfig +from .config_validator import ConfigValidator + +logger = logging.getLogger(__name__) + + +class ConfigLoader: + """配置加载器类""" + + CONFIG_FILENAME = "config.json" + TEMPLATE_FILENAME = "config.template.json" + + def __init__(self, config_dir: Optional[str] = None): + """ + 初始化配置加载器 + + Args: + config_dir: 配置文件目录,默认为可执行文件所在目录 + """ + self.config_dir = config_dir or self._get_executable_dir() + self.config_path = os.path.join(self.config_dir, self.CONFIG_FILENAME) + self.template_path = os.path.join(self.config_dir, self.TEMPLATE_FILENAME) + + @staticmethod + def _get_executable_dir() -> str: + """ + 获取可执行文件所在目录 + + Returns: + str: 可执行文件目录路径 + """ + if getattr(sys, 'frozen', False): + # PyInstaller打包后的可执行文件 + return os.path.dirname(sys.executable) + else: + # 开发环境 + return os.getcwd() + + def load_config(self) -> SearchConfig: + """ + 加载配置文件 + + Returns: + SearchConfig: 配置对象 + """ + # 首先尝试加载JSON配置文件 + if os.path.exists(self.config_path): + try: + config_dict = self._load_json_config() + return dict_to_search_config(config_dict) + except Exception as e: + logger.warning(f"加载配置文件失败: {e}") + logger.info("使用默认配置") + return dict_to_search_config(DEFAULT_CONFIG) + else: + logger.info("未找到配置文件,使用默认配置") + # 如果有模板文件,建议用户复制 + if os.path.exists(self.template_path): + logger.info(f"提示: 可以复制 {self.template_path} 为 {self.config_path} 来自定义配置") + return dict_to_search_config(DEFAULT_CONFIG) + + def _load_json_config(self) -> Dict[str, Any]: + """ + 加载JSON配置文件 + + Returns: + Dict[str, Any]: 配置字典 + + Raises: + ValueError: 配置文件格式错误或验证失败 + """ + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + config_dict = json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f"配置文件JSON格式错误: {e}") + except FileNotFoundError: + raise ValueError(f"配置文件不存在: {self.config_path}") + except Exception as e: + raise ValueError(f"读取配置文件失败: {e}") + + # 验证配置 + is_valid, errors = ConfigValidator.validate_full_config(config_dict) + if not is_valid: + error_msg = "配置文件验证失败:\\n" + "\\n".join(f"- {error}" for error in errors) + raise ValueError(error_msg) + + # 合并默认配置 + merged_config = self._merge_with_defaults(config_dict) + + logger.info(f"成功加载配置文件: {self.config_path}") + return merged_config + + def _merge_with_defaults(self, user_config: Dict[str, Any]) -> Dict[str, Any]: + """ + 将用户配置与默认配置合并 + + Args: + user_config: 用户配置字典 + + Returns: + Dict[str, Any]: 合并后的配置字典 + """ + merged_config = DEFAULT_CONFIG.copy() + + # 递归合并嵌套字典 + def merge_dict(default: Dict[str, Any], user: Dict[str, Any]) -> Dict[str, Any]: + result = default.copy() + for key, value in user.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = merge_dict(result[key], value) + else: + result[key] = value + return result + + return merge_dict(merged_config, user_config) + + def save_config(self, config: SearchConfig) -> None: + """ + 保存配置到文件 + + Args: + config: 要保存的配置对象 + """ + from .default_config import search_config_to_dict + + config_dict = search_config_to_dict(config) + + try: + # 确保配置目录存在 + os.makedirs(self.config_dir, exist_ok=True) + + with open(self.config_path, 'w', encoding='utf-8') as f: + json.dump(config_dict, f, indent=2, ensure_ascii=False) + + logger.info(f"配置已保存到: {self.config_path}") + except Exception as e: + logger.error(f"保存配置文件失败: {e}") + raise + + def create_template(self) -> None: + """ + 创建配置模板文件 + """ + try: + # 确保配置目录存在 + os.makedirs(self.config_dir, exist_ok=True) + + with open(self.template_path, 'w', encoding='utf-8') as f: + json.dump(DEFAULT_CONFIG, f, indent=2, ensure_ascii=False) + + logger.info(f"配置模板已创建: {self.template_path}") + except Exception as e: + logger.error(f"创建配置模板失败: {e}") + raise + + def get_config_info(self) -> Dict[str, Any]: + """ + 获取配置文件信息 + + Returns: + Dict[str, Any]: 配置信息 + """ + return { + "config_dir": self.config_dir, + "config_path": self.config_path, + "template_path": self.template_path, + "config_exists": os.path.exists(self.config_path), + "template_exists": os.path.exists(self.template_path), + "is_executable": getattr(sys, 'frozen', False) + } + + +def load_config_from_file(config_file_path: Optional[str] = None) -> SearchConfig: + """ + 便捷函数:从指定文件或默认位置加载配置 + + Args: + config_file_path: 配置文件路径,为None时使用默认位置 + + Returns: + SearchConfig: 配置对象 + """ + if config_file_path: + config_dir = os.path.dirname(config_file_path) + filename = os.path.basename(config_file_path) + loader = ConfigLoader(config_dir) + loader.CONFIG_FILENAME = filename + else: + loader = ConfigLoader() + + return loader.load_config() + + +def create_config_template(target_dir: Optional[str] = None) -> None: + """ + 便捷函数:创建配置模板文件 + + Args: + target_dir: 目标目录,为None时使用默认位置 + """ + loader = ConfigLoader(target_dir) + loader.create_template() \ No newline at end of file diff --git a/src/config/config_validator.py b/src/config/config_validator.py new file mode 100644 index 0000000..41ef7cc --- /dev/null +++ b/src/config/config_validator.py @@ -0,0 +1,228 @@ +""" +配置验证器模块 + +提供JSON配置文件的格式验证和数据校验功能 +""" + +import os +from typing import Dict, Any, List, Optional, Tuple +import logging + +logger = logging.getLogger(__name__) + + +class ConfigValidator: + """配置验证器类""" + + # JSON配置的模式定义 + CONFIG_SCHEMA = { + "type": "object", + "required": ["repo_path"], + "properties": { + "repo_path": {"type": "string"}, + "search_term": {"type": "string"}, + "is_regex": {"type": "boolean"}, + "validate": {"type": "boolean"}, + "validate_workers": {"type": "integer", "minimum": 1, "maximum": 32}, + "output": { + "type": "object", + "properties": { + "db_path": {"type": "string"}, + "excel_path": {"type": "string"} + } + }, + "logging": { + "type": "object", + "properties": { + "level": {"type": "string", "enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]} + } + }, + "filters": { + "type": "object", + "properties": { + "ignore_dirs": {"type": "array", "items": {"type": "string"}}, + "file_extensions": { + "oneOf": [ + {"type": "null"}, + {"type": "array", "items": {"type": "string"}} + ] + } + } + } + } + } + + @staticmethod + def validate_config(config_dict: Dict[str, Any]) -> Tuple[bool, List[str]]: + """ + 验证配置字典的格式和数据有效性 + + Args: + config_dict: 要验证的配置字典 + + Returns: + Tuple[bool, List[str]]: (是否有效, 错误信息列表) + """ + errors = [] + + # 基本类型检查 + if not isinstance(config_dict, dict): + errors.append("配置必须是一个字典对象") + return False, errors + + # 验证必需字段 + if "repo_path" not in config_dict: + errors.append("缺少必需字段: repo_path") + + # 验证repo_path + if "repo_path" in config_dict: + repo_path = config_dict["repo_path"] + if not isinstance(repo_path, str): + errors.append("repo_path必须是字符串类型") + elif not repo_path.strip(): + errors.append("repo_path不能为空") + + # 验证search_term + if "search_term" in config_dict: + search_term = config_dict["search_term"] + if not isinstance(search_term, str): + errors.append("search_term必须是字符串类型") + elif not search_term.strip(): + errors.append("search_term不能为空") + + # 验证布尔类型字段 + for field in ["is_regex", "validate"]: + if field in config_dict and not isinstance(config_dict[field], bool): + errors.append(f"{field}必须是布尔类型") + + # 验证validate_workers + if "validate_workers" in config_dict: + workers = config_dict["validate_workers"] + if not isinstance(workers, int): + errors.append("validate_workers必须是整数类型") + elif workers < 1 or workers > 32: + errors.append("validate_workers必须在1-32之间") + + # 验证output配置 + if "output" in config_dict: + output_config = config_dict["output"] + if not isinstance(output_config, dict): + errors.append("output配置必须是字典类型") + else: + for path_field in ["db_path", "excel_path"]: + if path_field in output_config: + path_value = output_config[path_field] + if not isinstance(path_value, str): + errors.append(f"output.{path_field}必须是字符串类型") + elif not path_value.strip(): + errors.append(f"output.{path_field}不能为空") + + # 验证logging配置 + if "logging" in config_dict: + logging_config = config_dict["logging"] + if not isinstance(logging_config, dict): + errors.append("logging配置必须是字典类型") + else: + if "level" in logging_config: + level = logging_config["level"] + valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + if not isinstance(level, str): + errors.append("logging.level必须是字符串类型") + elif level not in valid_levels: + errors.append(f"logging.level必须是以下值之一: {', '.join(valid_levels)}") + + # 验证filters配置 + if "filters" in config_dict: + filters_config = config_dict["filters"] + if not isinstance(filters_config, dict): + errors.append("filters配置必须是字典类型") + else: + # 验证ignore_dirs + if "ignore_dirs" in filters_config: + ignore_dirs = filters_config["ignore_dirs"] + if not isinstance(ignore_dirs, list): + errors.append("filters.ignore_dirs必须是数组类型") + else: + for i, dir_name in enumerate(ignore_dirs): + if not isinstance(dir_name, str): + errors.append(f"filters.ignore_dirs[{i}]必须是字符串类型") + + # 验证file_extensions + if "file_extensions" in filters_config: + file_extensions = filters_config["file_extensions"] + if file_extensions is not None: + if not isinstance(file_extensions, list): + errors.append("filters.file_extensions必须是数组或null") + else: + for i, ext in enumerate(file_extensions): + if not isinstance(ext, str): + errors.append(f"filters.file_extensions[{i}]必须是字符串类型") + + return len(errors) == 0, errors + + @staticmethod + def validate_paths(config_dict: Dict[str, Any]) -> Tuple[bool, List[str]]: + """ + 验证配置中的路径有效性 + + Args: + config_dict: 配置字典 + + Returns: + Tuple[bool, List[str]]: (是否有效, 错误信息列表) + """ + errors = [] + + # 验证repo_path是否存在 + if "repo_path" in config_dict: + repo_path = config_dict["repo_path"] + if isinstance(repo_path, str) and repo_path.strip(): + # 处理相对路径 + if not os.path.isabs(repo_path): + repo_path = os.path.abspath(repo_path) + + if not os.path.exists(repo_path): + errors.append(f"仓库路径不存在: {repo_path}") + elif not os.path.isdir(repo_path): + errors.append(f"仓库路径不是目录: {repo_path}") + + # 验证输出路径的父目录是否存在或可创建 + if "output" in config_dict and isinstance(config_dict["output"], dict): + output_config = config_dict["output"] + + for path_field in ["db_path", "excel_path"]: + if path_field in output_config: + path_value = output_config[path_field] + if isinstance(path_value, str) and path_value.strip(): + parent_dir = os.path.dirname(path_value) + if parent_dir and not os.path.exists(parent_dir): + try: + os.makedirs(parent_dir, exist_ok=True) + except OSError as e: + errors.append(f"无法创建{path_field}的父目录 {parent_dir}: {e}") + + return len(errors) == 0, errors + + @classmethod + def validate_full_config(cls, config_dict: Dict[str, Any]) -> Tuple[bool, List[str]]: + """ + 执行完整的配置验证 + + Args: + config_dict: 配置字典 + + Returns: + Tuple[bool, List[str]]: (是否有效, 错误信息列表) + """ + all_errors = [] + + # 格式验证 + format_valid, format_errors = cls.validate_config(config_dict) + all_errors.extend(format_errors) + + # 如果格式验证通过,再进行路径验证 + if format_valid: + path_valid, path_errors = cls.validate_paths(config_dict) + all_errors.extend(path_errors) + + return len(all_errors) == 0, all_errors \ No newline at end of file diff --git a/src/config/default_config.py b/src/config/default_config.py new file mode 100644 index 0000000..d4e64d1 --- /dev/null +++ b/src/config/default_config.py @@ -0,0 +1,129 @@ +""" +默认配置定义模块 + +定义系统的默认配置项和配置创建函数 +""" + +from typing import Dict, Any, List, Optional +from dataclasses import dataclass, field +import os + + +@dataclass +class SearchConfig: + """搜索配置数据类""" + repo_path: str = "." + search_term: str = "test,def,void" + is_regex: bool = False + validate: bool = False + validate_workers: int = 4 + db_path: str = "results.db" + excel_path: str = "results.xlsx" + log_level: str = "INFO" + ignore_dirs: List[str] = field(default_factory=lambda: [ + ".git", "__pycache__", ".svn", ".hg", ".idea", + ".vscode", "node_modules", ".tox", "dist", "build" + ]) + file_extensions: Optional[List[str]] = None + + +# 默认配置字典,用于JSON序列化 +DEFAULT_CONFIG: Dict[str, Any] = { + "repo_path": ".", + "search_term": "test,def,void", + "is_regex": False, + "validate": False, + "validate_workers": 4, + "output": { + "db_path": "results.db", + "excel_path": "results.xlsx" + }, + "logging": { + "level": "INFO" + }, + "filters": { + "ignore_dirs": [ + ".git", "__pycache__", ".svn", ".hg", ".idea", + ".vscode", "node_modules", ".tox", "dist", "build" + ], + "file_extensions": None + } +} + + +def create_default_config() -> SearchConfig: + """ + 创建默认配置对象 + + Returns: + SearchConfig: 默认配置实例 + """ + return SearchConfig() + + +def dict_to_search_config(config_dict: Dict[str, Any]) -> SearchConfig: + """ + 将配置字典转换为SearchConfig对象 + + Args: + config_dict: 配置字典 + + Returns: + SearchConfig: 配置对象 + """ + # 处理嵌套的output配置 + output_config = config_dict.get("output", {}) + db_path = output_config.get("db_path", DEFAULT_CONFIG["output"]["db_path"]) + excel_path = output_config.get("excel_path", DEFAULT_CONFIG["output"]["excel_path"]) + + # 处理嵌套的logging配置 + logging_config = config_dict.get("logging", {}) + log_level = logging_config.get("level", DEFAULT_CONFIG["logging"]["level"]) + + # 处理嵌套的filters配置 + filters_config = config_dict.get("filters", {}) + ignore_dirs = filters_config.get("ignore_dirs", DEFAULT_CONFIG["filters"]["ignore_dirs"]) + file_extensions = filters_config.get("file_extensions", DEFAULT_CONFIG["filters"]["file_extensions"]) + + return SearchConfig( + repo_path=config_dict.get("repo_path", DEFAULT_CONFIG["repo_path"]), + search_term=config_dict.get("search_term", DEFAULT_CONFIG["search_term"]), + is_regex=config_dict.get("is_regex", DEFAULT_CONFIG["is_regex"]), + validate=config_dict.get("validate", DEFAULT_CONFIG["validate"]), + validate_workers=config_dict.get("validate_workers", DEFAULT_CONFIG["validate_workers"]), + db_path=db_path, + excel_path=excel_path, + log_level=log_level, + ignore_dirs=ignore_dirs, + file_extensions=file_extensions + ) + + +def search_config_to_dict(config: SearchConfig) -> Dict[str, Any]: + """ + 将SearchConfig对象转换为字典 + + Args: + config: SearchConfig对象 + + Returns: + Dict[str, Any]: 配置字典 + """ + return { + "repo_path": config.repo_path, + "search_term": config.search_term, + "is_regex": config.is_regex, + "validate": config.validate, + "validate_workers": config.validate_workers, + "output": { + "db_path": config.db_path, + "excel_path": config.excel_path + }, + "logging": { + "level": config.log_level + }, + "filters": { + "ignore_dirs": config.ignore_dirs, + "file_extensions": config.file_extensions + } + } \ No newline at end of file diff --git a/src/packaging/__init__.py b/src/packaging/__init__.py new file mode 100644 index 0000000..db4ce58 --- /dev/null +++ b/src/packaging/__init__.py @@ -0,0 +1,15 @@ +""" +PyInstaller打包支持模块 + +提供PyInstaller钩子和资源打包功能 +""" + +from .pyinstaller_hooks import get_hidden_imports, get_hook_dirs +from .resource_bundler import ResourceBundler, bundle_resources + +__all__ = [ + 'get_hidden_imports', + 'get_hook_dirs', + 'ResourceBundler', + 'bundle_resources' +] \ No newline at end of file diff --git a/src/packaging/pyinstaller_hooks.py b/src/packaging/pyinstaller_hooks.py new file mode 100644 index 0000000..0c078e6 --- /dev/null +++ b/src/packaging/pyinstaller_hooks.py @@ -0,0 +1,279 @@ +""" +PyInstaller钩子模块 + +提供PyInstaller打包时需要的隐藏导入和钩子目录 +""" + +import os +import sys +from typing import List, Dict, Any +from pathlib import Path + + +def get_hidden_imports() -> List[str]: + """ + 获取PyInstaller需要的隐藏导入模块列表 + + Returns: + List[str]: 隐藏导入模块列表 + """ + hidden_imports = [ + # SQLAlchemy相关 + 'sqlalchemy', + 'sqlalchemy.ext.declarative', + 'sqlalchemy.orm', + 'sqlalchemy.sql', + 'sqlalchemy.dialects.sqlite', + 'sqlalchemy.pool', + 'sqlalchemy.engine', + + # Alembic相关 + 'alembic', + 'alembic.runtime.migration', + 'alembic.operations', + 'alembic.script', + + # pandas和Excel导出相关 + 'pandas', + 'pandas.io.excel', + 'openpyxl', + 'openpyxl.workbook', + 'openpyxl.worksheet', + 'openpyxl.styles', + + # loguru日志相关 + 'loguru', + + # JSON处理 + 'json', + + # 正则表达式 + 're', + + # 文件系统操作 + 'pathlib', + 'os', + 'os.path', + + # 并发处理 + 'concurrent.futures', + 'threading', + 'multiprocessing', + + # 数据结构 + 'dataclasses', + 'typing', + 'collections', + + # 时间处理 + 'datetime', + + # 编码相关 + 'chardet', + + # 系统相关 + 'sys', + 'platform', + ] + + return hidden_imports + + +def get_hook_dirs() -> List[str]: + """ + 获取PyInstaller钩子目录列表 + + Returns: + List[str]: 钩子目录列表 + """ + current_dir = Path(__file__).parent + hooks_dir = current_dir / "hooks" + + if hooks_dir.exists(): + return [str(hooks_dir)] + return [] + + +def get_data_files() -> List[tuple]: + """ + 获取需要打包的数据文件列表 + + Returns: + List[tuple]: 数据文件列表,格式为[(source, destination), ...] + """ + data_files = [] + + # 项目根目录 + project_root = Path(__file__).parent.parent.parent + + # 配置模板文件 + config_dir = project_root / "config" + if config_dir.exists(): + template_file = config_dir / "config.template.json" + default_file = config_dir / "default.json" + + if template_file.exists(): + data_files.append((str(template_file), "config.template.json")) + if default_file.exists(): + data_files.append((str(default_file), "config/default.json")) + + # 数据库迁移文件(如果存在) + migrations_dir = project_root / "src" / "database" / "migrations" + if migrations_dir.exists(): + for migration_file in migrations_dir.glob("*.py"): + if migration_file.name != "__init__.py": + data_files.append((str(migration_file), f"database/migrations/{migration_file.name}")) + + return data_files + + +def get_exclude_modules() -> List[str]: + """ + 获取需要排除的模块列表 + + Returns: + List[str]: 排除的模块列表 + """ + exclude_modules = [ + # 测试相关模块 + 'pytest', + 'unittest', + 'test', + 'tests', + + # 开发工具 + 'pip', + 'setuptools', + 'wheel', + 'distutils', + + # 调试工具 + 'pdb', + 'ipdb', + 'debugpy', + + # 文档工具 + 'sphinx', + 'docutils', + + # Jupyter相关 + 'jupyter', + 'ipython', + 'notebook', + + # IDE相关 + 'pylsp', + 'pyls', + + # 不需要的GUI库 + 'tkinter', + 'matplotlib', + 'pyplot', + + # 不需要的网络库 + 'requests', + 'urllib3', + 'http', + 'email', + + # 不需要的科学计算库 + 'numpy', + 'scipy', + 'sklearn', + ] + + return exclude_modules + + +def get_collect_submodules() -> List[str]: + """ + 获取需要收集所有子模块的包列表 + + Returns: + List[str]: 包列表 + """ + return [ + 'sqlalchemy', + 'alembic', + 'pandas', + 'openpyxl', + 'loguru', + ] + + +def create_spec_options() -> Dict[str, Any]: + """ + 创建PyInstaller .spec文件的选项字典 + + Returns: + Dict[str, Any]: spec文件选项 + """ + options = { + 'hiddenimports': get_hidden_imports(), + 'excludes': get_exclude_modules(), + 'datas': get_data_files(), + 'collect_submodules': get_collect_submodules(), + 'noarchive': False, + 'optimize': 2, + 'strip': False, + 'upx': True, + 'upx_exclude': [], + 'runtime_tmpdir': None, + 'console': True, + 'disable_windowed_traceback': False, + 'target_arch': None, + 'codesign_identity': None, + 'entitlements_file': None, + } + + return options + + +def get_analysis_options() -> Dict[str, Any]: + """ + 获取Analysis阶段的选项 + + Returns: + Dict[str, Any]: Analysis选项 + """ + return { + 'pathex': [], + 'binaries': [], + 'datas': get_data_files(), + 'hiddenimports': get_hidden_imports(), + 'hookspath': get_hook_dirs(), + 'hooksconfig': {}, + 'runtime_hooks': [], + 'excludes': get_exclude_modules(), + 'win_no_prefer_redirects': False, + 'win_private_assemblies': False, + 'cipher': None, + 'noarchive': False, + } + + +def get_exe_options(name: str = "hello-scan-code") -> Dict[str, Any]: + """ + 获取EXE阶段的选项 + + Args: + name: 可执行文件名称 + + Returns: + Dict[str, Any]: EXE选项 + """ + return { + 'name': name, + 'debug': False, + 'bootloader_ignore_signals': False, + 'strip': False, + 'upx': True, + 'upx_exclude': [], + 'runtime_tmpdir': None, + 'console': True, + 'disable_windowed_traceback': False, + 'target_arch': None, + 'codesign_identity': None, + 'entitlements_file': None, + 'icon': None, # 可以在这里指定图标文件路径 + } \ No newline at end of file diff --git a/src/packaging/resource_bundler.py b/src/packaging/resource_bundler.py new file mode 100644 index 0000000..59daaea --- /dev/null +++ b/src/packaging/resource_bundler.py @@ -0,0 +1,297 @@ +""" +资源打包器模块 + +提供资源文件的收集和打包功能 +""" + +import os +import shutil +import json +from typing import List, Dict, Any, Optional +from pathlib import Path +import logging + +logger = logging.getLogger(__name__) + + +class ResourceBundler: + """资源打包器类""" + + def __init__(self, project_root: Optional[str] = None): + """ + 初始化资源打包器 + + Args: + project_root: 项目根目录,默认为当前文件的项目根目录 + """ + if project_root is None: + self.project_root = Path(__file__).parent.parent.parent + else: + self.project_root = Path(project_root) + + def collect_config_files(self) -> List[tuple]: + """ + 收集配置文件 + + Returns: + List[tuple]: [(源文件路径, 目标路径), ...] + """ + data_files = [] + config_dir = self.project_root / "config" + + if config_dir.exists(): + # 配置模板文件 + template_file = config_dir / "config.template.json" + if template_file.exists(): + data_files.append((str(template_file), "config.template.json")) + + # 默认配置文件 + default_file = config_dir / "default.json" + if default_file.exists(): + data_files.append((str(default_file), "default.json")) + + return data_files + + def collect_database_files(self) -> List[tuple]: + """ + 收集数据库相关文件 + + Returns: + List[tuple]: [(源文件路径, 目标路径), ...] + """ + data_files = [] + + # 数据库迁移文件 + migrations_dir = self.project_root / "src" / "database" / "migrations" + if migrations_dir.exists(): + for migration_file in migrations_dir.glob("*.py"): + if migration_file.name != "__init__.py": + relative_path = f"database/migrations/{migration_file.name}" + data_files.append((str(migration_file), relative_path)) + + # 数据库配置文件 + db_config_dir = self.project_root / "src" / "database" / "config" + if db_config_dir.exists(): + for config_file in db_config_dir.glob("*.py"): + if config_file.name != "__init__.py": + relative_path = f"database/config/{config_file.name}" + data_files.append((str(config_file), relative_path)) + + return data_files + + def collect_template_files(self) -> List[tuple]: + """ + 收集模板文件 + + Returns: + List[tuple]: [(源文件路径, 目标路径), ...] + """ + data_files = [] + + # 搜索模板文件 + templates_dir = self.project_root / "templates" + if templates_dir.exists(): + for template_file in templates_dir.rglob("*"): + if template_file.is_file(): + relative_path = template_file.relative_to(templates_dir) + target_path = f"templates/{relative_path}" + data_files.append((str(template_file), target_path)) + + return data_files + + def collect_license_files(self) -> List[tuple]: + """ + 收集许可证和文档文件 + + Returns: + List[tuple]: [(源文件路径, 目标路径), ...] + """ + data_files = [] + + # 许可证文件 + license_files = ["LICENSE", "LICENSE.txt", "LICENSE.md"] + for license_file in license_files: + license_path = self.project_root / license_file + if license_path.exists(): + data_files.append((str(license_path), license_file)) + break + + # README文件 + readme_files = ["README.md", "README.txt", "README.rst"] + for readme_file in readme_files: + readme_path = self.project_root / readme_file + if readme_path.exists(): + data_files.append((str(readme_path), readme_file)) + break + + return data_files + + def collect_all_resources(self) -> List[tuple]: + """ + 收集所有资源文件 + + Returns: + List[tuple]: [(源文件路径, 目标路径), ...] + """ + all_data_files = [] + + # 收集各类资源文件 + all_data_files.extend(self.collect_config_files()) + all_data_files.extend(self.collect_database_files()) + all_data_files.extend(self.collect_template_files()) + all_data_files.extend(self.collect_license_files()) + + # 去重 + unique_data_files = [] + seen = set() + for source, target in all_data_files: + if target not in seen: + unique_data_files.append((source, target)) + seen.add(target) + + logger.info(f"收集到 {len(unique_data_files)} 个资源文件") + return unique_data_files + + def create_resource_manifest(self, output_path: Optional[str] = None) -> str: + """ + 创建资源清单文件 + + Args: + output_path: 输出路径,默认为项目根目录下的resource_manifest.json + + Returns: + str: 清单文件路径 + """ + if output_path is None: + output_path = self.project_root / "resource_manifest.json" + else: + output_path = Path(output_path) + + resources = self.collect_all_resources() + + manifest = { + "version": "1.0", + "generated_by": "ResourceBundler", + "total_files": len(resources), + "resources": { + "config_files": self.collect_config_files(), + "database_files": self.collect_database_files(), + "template_files": self.collect_template_files(), + "license_files": self.collect_license_files() + }, + "all_files": resources + } + + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(manifest, f, indent=2, ensure_ascii=False) + + logger.info(f"资源清单已保存到: {output_path}") + return str(output_path) + + def validate_resources(self) -> Dict[str, Any]: + """ + 验证资源文件的完整性 + + Returns: + Dict[str, Any]: 验证结果 + """ + resources = self.collect_all_resources() + missing_files = [] + existing_files = [] + + for source, target in resources: + if os.path.exists(source): + existing_files.append((source, target)) + else: + missing_files.append((source, target)) + + result = { + "total_files": len(resources), + "existing_files": len(existing_files), + "missing_files": len(missing_files), + "missing_list": missing_files, + "is_valid": len(missing_files) == 0 + } + + if missing_files: + logger.warning(f"发现 {len(missing_files)} 个缺失的资源文件") + for source, target in missing_files: + logger.warning(f" 缺失: {source} -> {target}") + else: + logger.info("所有资源文件验证通过") + + return result + + def copy_resources_to_dist(self, dist_dir: str) -> bool: + """ + 将资源文件复制到分发目录 + + Args: + dist_dir: 分发目录路径 + + Returns: + bool: 是否成功 + """ + try: + dist_path = Path(dist_dir) + dist_path.mkdir(parents=True, exist_ok=True) + + resources = self.collect_all_resources() + + for source, target in resources: + if os.path.exists(source): + target_path = dist_path / target + target_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target_path) + logger.debug(f"复制: {source} -> {target_path}") + + logger.info(f"成功复制 {len(resources)} 个资源文件到 {dist_dir}") + return True + + except Exception as e: + logger.error(f"复制资源文件失败: {e}") + return False + + +def bundle_resources(project_root: Optional[str] = None) -> List[tuple]: + """ + 便捷函数:打包所有资源文件 + + Args: + project_root: 项目根目录 + + Returns: + List[tuple]: 资源文件列表 + """ + bundler = ResourceBundler(project_root) + return bundler.collect_all_resources() + + +def create_resource_manifest(project_root: Optional[str] = None, + output_path: Optional[str] = None) -> str: + """ + 便捷函数:创建资源清单 + + Args: + project_root: 项目根目录 + output_path: 输出路径 + + Returns: + str: 清单文件路径 + """ + bundler = ResourceBundler(project_root) + return bundler.create_resource_manifest(output_path) + + +def validate_resources(project_root: Optional[str] = None) -> Dict[str, Any]: + """ + 便捷函数:验证资源文件 + + Args: + project_root: 项目根目录 + + Returns: + Dict[str, Any]: 验证结果 + """ + bundler = ResourceBundler(project_root) + return bundler.validate_resources() \ No newline at end of file diff --git a/test_config_system.py b/test_config_system.py new file mode 100644 index 0000000..038a71e --- /dev/null +++ b/test_config_system.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +""" +配置系统功能测试脚本 +""" + +import os +import sys +import json +import tempfile +import shutil +from pathlib import Path + +# 添加项目路径 +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root / "src")) + +from src.config.config_loader import ConfigLoader, load_config_from_file +from src.config.config_validator import ConfigValidator +from src.config.default_config import DEFAULT_CONFIG, SearchConfig, dict_to_search_config + + +def test_config_validator(): + """测试配置验证器""" + print("=== 测试配置验证器 ===") + + # 测试有效配置 + valid_config = DEFAULT_CONFIG.copy() + is_valid, errors = ConfigValidator.validate_config(valid_config) + print(f"默认配置验证: {'通过' if is_valid else '失败'}") + if errors: + print(f"错误: {errors}") + + # 测试无效配置 + invalid_config = { + "repo_path": "", # 空路径 + "validate_workers": -1, # 无效数值 + "logging": { + "level": "INVALID" # 无效日志级别 + } + } + is_valid, errors = ConfigValidator.validate_config(invalid_config) + print(f"无效配置验证: {'失败' if not is_valid else '意外通过'}") + print(f"预期错误数: {len(errors)}") + + print("配置验证器测试完成\n") + + +def test_config_loader(): + """测试配置加载器""" + print("=== 测试配置加载器 ===") + + # 创建临时目录 + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # 测试默认配置加载 + loader = ConfigLoader(temp_dir) + config = loader.load_config() + print(f"默认配置加载: 成功") + print(f"仓库路径: {config.repo_path}") + print(f"搜索词: {config.search_term}") + + # 创建自定义配置文件 + custom_config = { + "repo_path": "/test/path", + "search_term": "custom,search,terms", + "is_regex": True, + "validate": True, + "validate_workers": 8, + "output": { + "db_path": "custom.db", + "excel_path": "custom.xlsx" + }, + "logging": { + "level": "DEBUG" + }, + "filters": { + "ignore_dirs": [".git", "custom_ignore"], + "file_extensions": [".py", ".js"] + } + } + + config_file = temp_path / "config.json" + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(custom_config, f, indent=2) + + # 测试自定义配置加载 + loader = ConfigLoader(temp_dir) + try: + custom_loaded_config = loader.load_config() + print(f"自定义配置加载: 成功") + print(f"自定义仓库路径: {custom_loaded_config.repo_path}") + print(f"自定义搜索词: {custom_loaded_config.search_term}") + print(f"正则表达式: {custom_loaded_config.is_regex}") + print(f"验证模式: {custom_loaded_config.validate}") + print(f"工作线程数: {custom_loaded_config.validate_workers}") + except Exception as e: + print(f"自定义配置加载失败: {e}") + + # 测试配置模板创建 + loader.create_template() + template_path = temp_path / "config.template.json" + print(f"配置模板创建: {'成功' if template_path.exists() else '失败'}") + + print("配置加载器测试完成\n") + + +def test_config_conversion(): + """测试配置转换功能""" + print("=== 测试配置转换功能 ===") + + # 测试字典到SearchConfig的转换 + config_dict = DEFAULT_CONFIG.copy() + search_config = dict_to_search_config(config_dict) + + print(f"字典转换为SearchConfig: 成功") + print(f"类型: {type(search_config)}") + print(f"仓库路径: {search_config.repo_path}") + print(f"忽略目录数量: {len(search_config.ignore_dirs)}") + + # 测试SearchConfig到字典的转换 + from src.config.default_config import search_config_to_dict + converted_dict = search_config_to_dict(search_config) + + print(f"SearchConfig转换为字典: 成功") + print(f"字典键数量: {len(converted_dict)}") + + print("配置转换功能测试完成\n") + + +def test_compatibility(): + """测试向后兼容性""" + print("=== 测试向后兼容性 ===") + + # 测试原有parse_args函数 + try: + from src.config import parse_args + config = parse_args() + print(f"parse_args兼容性: 成功") + print(f"配置类型: {type(config)}") + print(f"仓库路径: {config.repo_path}") + except Exception as e: + print(f"parse_args兼容性测试失败: {e}") + + # 测试新的JSON配置加载函数 + try: + from src.config import load_json_config + json_config = load_json_config() + print(f"load_json_config功能: 成功") + print(f"配置类型: {type(json_config)}") + except Exception as e: + print(f"load_json_config测试失败: {e}") + + print("向后兼容性测试完成\n") + + +def test_error_handling(): + """测试错误处理""" + print("=== 测试错误处理 ===") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # 创建无效的JSON文件 + invalid_json_file = temp_path / "config.json" + with open(invalid_json_file, 'w') as f: + f.write("{ invalid json content }") + + loader = ConfigLoader(temp_dir) + try: + config = loader.load_config() + print(f"无效JSON处理: 成功回退到默认配置") + except Exception as e: + print(f"无效JSON处理失败: {e}") + + # 创建验证失败的配置文件 + invalid_config = { + "repo_path": "", # 空路径应该失败 + "validate_workers": "invalid" # 类型错误 + } + + with open(invalid_json_file, 'w') as f: + json.dump(invalid_config, f) + + try: + config = loader.load_config() + print(f"配置验证失败处理: 成功回退到默认配置") + except Exception as e: + print(f"配置验证失败处理失败: {e}") + + print("错误处理测试完成\n") + + +def main(): + """主函数""" + print("开始配置系统功能测试\n") + + try: + test_config_validator() + test_config_loader() + test_config_conversion() + test_compatibility() + test_error_handling() + + print("所有配置系统测试完成!") + + except Exception as e: + print(f"测试过程中发生错误: {e}") + return False + + return True + + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/test_packaging.py b/test_packaging.py new file mode 100644 index 0000000..ca93d5c --- /dev/null +++ b/test_packaging.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +""" +PyInstaller打包功能测试脚本 +""" + +import os +import sys +import subprocess +import shutil +from pathlib import Path +import importlib.util + + +def check_python_version(): + """检查Python版本""" + print("=== 检查Python环境 ===") + version = sys.version_info + print(f"Python版本: {version.major}.{version.minor}.{version.micro}") + + if version < (3, 10): + print("警告: Python版本过低,建议使用3.10+") + return False + else: + print("Python版本检查: 通过") + return True + + +def check_dependencies(): + """检查项目依赖""" + print("\n=== 检查项目依赖 ===") + + required_packages = { + 'loguru': 'loguru', + 'pandas': 'pandas', + 'openpyxl': 'openpyxl', + 'sqlalchemy': 'sqlalchemy', + 'alembic': 'alembic' + } + + missing_packages = [] + + for package_name, import_name in required_packages.items(): + try: + spec = importlib.util.find_spec(import_name) + if spec is not None: + print(f"✓ {package_name}: 已安装") + else: + print(f"✗ {package_name}: 未找到") + missing_packages.append(package_name) + except ImportError: + print(f"✗ {package_name}: 导入失败") + missing_packages.append(package_name) + + if missing_packages: + print(f"\n缺少依赖包: {', '.join(missing_packages)}") + print("请安装缺少的依赖包") + return False + else: + print("\n所有依赖包检查: 通过") + return True + + +def check_pyinstaller(): + """检查PyInstaller""" + print("\n=== 检查PyInstaller ===") + + try: + import PyInstaller + print(f"PyInstaller版本: {PyInstaller.__version__}") + + # 检查PyInstaller命令行工具 + result = subprocess.run([sys.executable, "-m", "PyInstaller", "--version"], + capture_output=True, text=True) + if result.returncode == 0: + print("PyInstaller命令行工具: 可用") + return True + else: + print("PyInstaller命令行工具: 不可用") + return False + except ImportError: + print("PyInstaller: 未安装") + print("请执行: pip install pyinstaller") + return False + + +def test_project_structure(): + """测试项目结构""" + print("\n=== 检查项目结构 ===") + + project_root = Path(__file__).parent + + required_files = [ + "src/main.py", + "src/config/__init__.py", + "src/config/config_loader.py", + "src/config/config_validator.py", + "src/config/default_config.py", + "src/packaging/__init__.py", + "src/packaging/pyinstaller_hooks.py", + "src/packaging/resource_bundler.py", + "config/config.template.json", + "config/default.json", + "build/windows/hello-scan-code.spec", + "build/linux/hello-scan-code.spec", + "scripts/build_windows.py", + "scripts/build_linux.py" + ] + + missing_files = [] + + for file_path in required_files: + full_path = project_root / file_path + if full_path.exists(): + print(f"✓ {file_path}") + else: + print(f"✗ {file_path}") + missing_files.append(file_path) + + if missing_files: + print(f"\n缺少文件: {len(missing_files)}个") + return False + else: + print("\n项目结构检查: 通过") + return True + + +def test_import_modules(): + """测试模块导入""" + print("\n=== 测试模块导入 ===") + + test_modules = [ + "src.config.default_config", + "src.config.config_loader", + "src.config.config_validator", + "src.packaging.pyinstaller_hooks", + "src.packaging.resource_bundler" + ] + + project_root = Path(__file__).parent + sys.path.insert(0, str(project_root)) + + failed_imports = [] + + for module_name in test_modules: + try: + importlib.import_module(module_name) + print(f"✓ {module_name}") + except ImportError as e: + print(f"✗ {module_name}: {e}") + failed_imports.append(module_name) + + if failed_imports: + print(f"\n导入失败: {len(failed_imports)}个模块") + return False + else: + print("\n模块导入测试: 通过") + return True + + +def test_resource_bundler(): + """测试资源打包器""" + print("\n=== 测试资源打包器 ===") + + try: + project_root = Path(__file__).parent + sys.path.insert(0, str(project_root)) + + from src.packaging.resource_bundler import ResourceBundler + + bundler = ResourceBundler(str(project_root)) + + # 测试资源收集 + config_files = bundler.collect_config_files() + print(f"配置文件: {len(config_files)}个") + + all_resources = bundler.collect_all_resources() + print(f"总资源文件: {len(all_resources)}个") + + # 测试资源验证 + validation = bundler.validate_resources() + print(f"资源验证: {'通过' if validation['is_valid'] else '失败'}") + if not validation['is_valid']: + print(f"缺失文件: {validation['missing_files']}") + + return validation['is_valid'] + + except Exception as e: + print(f"资源打包器测试失败: {e}") + return False + + +def test_pyinstaller_hooks(): + """测试PyInstaller钩子""" + print("\n=== 测试PyInstaller钩子 ===") + + try: + project_root = Path(__file__).parent + sys.path.insert(0, str(project_root)) + + from src.packaging.pyinstaller_hooks import ( + get_hidden_imports, get_analysis_options, + get_exe_options, get_data_files + ) + + hidden_imports = get_hidden_imports() + print(f"隐藏导入模块: {len(hidden_imports)}个") + + analysis_options = get_analysis_options() + print(f"Analysis选项: {len(analysis_options)}个配置项") + + exe_options = get_exe_options() + print(f"EXE选项: {len(exe_options)}个配置项") + + data_files = get_data_files() + print(f"数据文件: {len(data_files)}个") + + print("PyInstaller钩子测试: 通过") + return True + + except Exception as e: + print(f"PyInstaller钩子测试失败: {e}") + return False + + +def test_spec_files(): + """测试spec文件语法""" + print("\n=== 测试spec文件语法 ===") + + project_root = Path(__file__).parent + spec_files = [ + project_root / "build" / "windows" / "hello-scan-code.spec", + project_root / "build" / "linux" / "hello-scan-code.spec" + ] + + for spec_file in spec_files: + if spec_file.exists(): + try: + # 简单语法检查 + with open(spec_file, 'r', encoding='utf-8') as f: + content = f.read() + + # 检查关键字段 + required_keywords = ['Analysis', 'PYZ', 'EXE'] + missing_keywords = [] + + for keyword in required_keywords: + if keyword not in content: + missing_keywords.append(keyword) + + if missing_keywords: + print(f"✗ {spec_file.name}: 缺少关键字段 {missing_keywords}") + return False + else: + print(f"✓ {spec_file.name}: 语法检查通过") + except Exception as e: + print(f"✗ {spec_file.name}: 读取失败 {e}") + return False + else: + print(f"✗ {spec_file.name}: 文件不存在") + return False + + print("spec文件测试: 通过") + return True + + +def main(): + """主函数""" + print("开始PyInstaller打包功能测试\n") + + tests = [ + ("Python版本检查", check_python_version), + ("依赖包检查", check_dependencies), + ("PyInstaller检查", check_pyinstaller), + ("项目结构检查", test_project_structure), + ("模块导入测试", test_import_modules), + ("资源打包器测试", test_resource_bundler), + ("PyInstaller钩子测试", test_pyinstaller_hooks), + ("spec文件测试", test_spec_files) + ] + + passed = 0 + total = len(tests) + + for test_name, test_func in tests: + try: + if test_func(): + passed += 1 + except Exception as e: + print(f"{test_name} 执行失败: {e}") + + print(f"\n=== 测试总结 ===") + print(f"通过: {passed}/{total}") + print(f"成功率: {passed/total*100:.1f}%") + + if passed == total: + print("\n所有测试通过!PyInstaller打包准备就绪。") + return True + else: + print(f"\n{total-passed}个测试失败,请检查并修复问题。") + return False + + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file