diff --git a/.gitignore b/.gitignore index e72aadd..f13369f 100644 --- a/.gitignore +++ b/.gitignore @@ -177,4 +177,7 @@ poetry.toml # LSP config files pyrightconfig.json -# End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file +# Claude Code files +.claude/ + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/CLAUDE.md b/CLAUDE.md index 0ca9ef1..ae4b67f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,18 @@ PyBuild-Generate 是一个基于 Textual TUI 框架的跨平台 Python 编译脚 uv sync ``` +### 代码质量检查 +```bash +# 类型检查 +ty check + +# 代码规范检查 +ruff check + +# 自动修复代码规范问题 +ruff check --fix +``` + ### 运行程序 ```bash # 推荐 @@ -35,18 +47,28 @@ uv run build_pyinstaller.py ### CI/CD 触发 提交信息包含特定前缀时自动触发 GitHub Actions 构建: -- `build_0:` 或 `build:` - PyInstaller 构建 -- `build_1:` - Nuitka 构建 +- `build:` - 同时触发 PyInstaller 和 Nuitka 构建 +- `build_0:` - 仅 PyInstaller 构建 +- `build_1:` - 仅 Nuitka 构建 + +**示例**: +```bash +git commit -m "build: 发布新版本" # 同时构建 +git commit -m "build_0: 修复界面问题" # 仅 PyInstaller +git commit -m "build_1: 性能优化" # 仅 Nuitka +``` + +构建产物在 GitHub Actions → Artifacts 下载,保留 7 天。 ## 架构设计 ### 应用架构 - **入口点**: `main.py` → `src/__main__.py` → `src/app.py` - **应用类**: `PyBuildTUI` 继承自 `textual.app.App`,管理屏幕导航、主题切换和配置持久化 -- **屏幕系统**: 9 个独立屏幕模块(见 `src/screens/`),每个屏幕处理特定功能步骤 +- **屏幕系统**: 12 个独立屏幕模块(见 `src/screens/`),每个屏幕处理特定功能步骤 ### 屏幕流程 -1. `WelcomeScreen` - 欢迎/选择功能模式 +1. `WelcomeScreen` - 欢迎/选择功能模式(生成构建脚本/生成安装包脚本) 2. `ProjectSelectorScreen` - 选择项目目录 3. `ModeSelectorScreen` - 选择构建模式(simple/full/expert) 4. `CompilerSelectorScreen` - 选择 PyInstaller 或 Nuitka @@ -54,7 +76,14 @@ uv run build_pyinstaller.py 6. `PackageOptionsScreen` - 配置数据文件、导入等高级选项 7. `PluginSelectorScreen` - 选择 Nuitka 插件 8. `InstallerConfigScreen` / `InstallerOptionsScreen` - 配置安装包(Inno Setup) -9. `GenerationScreen` - 生成最终脚本 +9. `InstallerGenerationScreen` - 生成安装包脚本 +10. `GenerationScreen` - 生成最终脚本 +11. `HelpScreen` - 帮助屏幕 + +**屏幕导航模式**: +- 使用 `self.app.push_screen()` 进入下一屏幕 +- 使用 `self.app.pop_screen()` 返回上一屏幕 +- ESC 键绑定为 `app.pop_screen()`,统一返回行为 ### 核心工具模块 @@ -74,6 +103,12 @@ uv run build_pyinstaller.py **`src/utils/config.py`** - 管理应用级配置 (`config.yaml`):主题、终端尺寸限制 +- `load_config()` / `save_config()` - 配置的加载和保存 + +**`src/widgets/option_builders.py`** +- UI 组件工厂函数:`create_switch_widget()`, `create_input_widget()`, `create_button_row()` +- `build_nuitka_options()` / `build_pyinstaller_options()` - 构建标签页组件 +- 可复用的 UI 布局生成器,减少重复代码 ### 主题系统 应用支持 8 种主题,通过 F1-F8 快捷键切换: @@ -88,6 +123,14 @@ uv run build_pyinstaller.py 主题选择会持久化到 `config.yaml`。 +### 快捷键 +| 快捷键 | 功能 | +|--------|------| +| `F1-F8` | 切换主题 | +| `ESC` | 返回上一步 | +| `Ctrl+C` | 退出程序 | +| `Ctrl+S` | 保存配置 | + ### 跨平台路径处理 生成的构建脚本使用 `os.path.join()` 处理路径,确保跨平台兼容性。对于数据文件的源/目标分隔符: - PyInstaller 使用 `;` (Windows) 或 `:` (Unix) @@ -111,8 +154,23 @@ PyBuild-Generate/ ├── build_*.py # 本项目的构建脚本 ├── src/ │ ├── __main__.py # 模块入口 +│ ├── __init__.py # 包初始化(定义 __version__, __author__, __repo__) │ ├── app.py # PyBuildTUI 主应用类 -│ ├── screens/ # TUI 屏幕模块(9个) +│ ├── screens/ # TUI 屏幕模块(12个) +│ │ ├── welcome_screen.py +│ │ ├── project_selector_screen.py +│ │ ├── mode_selector_screen.py +│ │ ├── compiler_selector_screen.py +│ │ ├── compile_config_screen.py +│ │ ├── package_options_screen.py +│ │ ├── plugin_selector_screen.py +│ │ ├── installer_config_screen.py +│ │ ├── installer_options_screen.py +│ │ ├── installer_generation_screen.py +│ │ ├── generation_screen.py +│ │ └── help_screen.py +│ ├── widgets/ # 可复用 UI 组件 +│ │ └── option_builders.py # UI 组件工厂函数 │ └── utils/ # 工具模块 │ ├── build_config.py # 构建配置管理 │ ├── script_generator.py # 构建脚本生成 @@ -126,6 +184,12 @@ PyBuild-Generate/ ## 开发注意事项 +### 代码组织原则 +- **模块化**:每个屏幕模块职责单一,不超过 600 行 +- **可复用组件**:通用 UI 组件抽取到 `src/widgets/option_builders.py` +- **配置驱动**:所有构建参数通过 `DEFAULT_BUILD_CONFIG` ��一定义 +- **异步操作**:配置加载/保存使用异步函数避免阻塞 UI + ### 添加新的构建参数 1. 更新 `DEFAULT_BUILD_CONFIG` in `src/utils/build_config.py` 2. 在 `script_generator.py` 中对应生成函数添加参数处理 @@ -136,6 +200,14 @@ PyBuild-Generate/ 2. 在 `src/screens/__init__.py` 中导出 3. 使用 `self.app.push_screen()` 或 `self.app.pop_screen()` 导航 +### 数据共享模式 +屏幕间共享数据通过 `self.app` 属性: +- `self.app.project_dir` - 当前选中的项目目录 +- `self.app.build_mode` - 构建模式(simple/full/expert) +- `self.app.config` - 应用级配置字典(主题、终端尺寸限制) + +**注意**: `build_config`(项目构建配置)存储在项目目录的 `build_config.yaml` 中,通过 `load_build_config()` / `save_build_config()` 函数操作,不作为 `self.app` 属性。 + ### 配置文件格式 - `config.yaml` - 应用级配置(键: 值) - `build_config.yaml` - 项目级配置,支持注释、列表(用 `- ` 前缀) @@ -143,3 +215,31 @@ PyBuild-Generate/ ### Python 版本要求 - 运行本工具: Python >= 3.12 - 生成的脚本: Python >= 3.6(推荐 3.8+),使用 f-string 语法 + +## 调试和测试 + +### 本地测试生成的脚本 +生成脚本后,可以在目标项目中运行: +```bash +# Windows +cd path\to\target\project +python build_nuitka.py + +# Linux/macOS +cd /path/to/target/project +python build_nuitka.py +``` + +### 验证配置加载 +检查 `build_config.yaml` 是否正确生成: +```bash +# Windows +type build_config.yaml + +# Linux/macOS +cat build_config.yaml + +# 使用本工具重新加载 +uv run main.py +# 选择项目目录 → 查看配置是否回显正确 +``` diff --git a/PyBuilder_setup.iss b/PyBuilder_setup.iss index 01c12aa..dac1321 100644 --- a/PyBuilder_setup.iss +++ b/PyBuilder_setup.iss @@ -22,7 +22,7 @@ CreateAppDir=yes ; 输出设置 OutputDir=dist -OutputBaseFilename={#MyAppName}_V{#MyAppVersion}_Setup +OutputBaseFilename={#MyAppName}_V{#MyAppVersion}_Setup-Beta SetupIconFile=assets/app.ico ; 卸载设置 @@ -60,7 +60,7 @@ Name: "chinesesimp"; MessagesFile: "compiler:Default.isl" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] -Source: "build\PyBuilder\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "build\PyBuilder.dist\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" diff --git a/README.md b/README.md index cb1f08c..d6dfd26 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ PyBuild-Generate/ ├── build_*.py # 构建脚本 ├── src/ │ ├── app.py # 主应用 -│ ├── screens/ # 8个界面屏幕 +│ ├── screens/ # 12个界面屏幕 │ └── utils/ # 工具模块 ├── .github/workflows/ # CI/CD 配置 └── assets/ # 资源文件 @@ -154,9 +154,10 @@ PyBuild-Generate/ ### 运行本工具需要 - Python >= 3.12 -- textual >= 6.8.0 +- textual >= 6.12.0 - pyfiglet >= 1.0.4 - loguru >= 0.7.3 +- pyyaml >= 6.0 - nuitka >= 2.8.9 - pyinstaller >= 6.17.0 diff --git a/build_config.yaml b/build_config.yaml index f5e85a6..420bcbc 100644 --- a/build_config.yaml +++ b/build_config.yaml @@ -35,13 +35,14 @@ installer_app_name: PyBuilder installer_version: 1.0.0 installer_publisher: ASLant installer_exe_name: PyBuilder.exe -installer_source_dir: build\PyBuilder +installer_source_dir: build\PyBuilder.dist installer_output_dir: dist installer_icon: assets/app.ico installer_appid: 3570A6C9-D68D-41D0-A6A0-4C3E3E4F8ECF installer_privileges: dialog installer_compression: lzma2/ultra64 installer_path_scope: user +installer_custom_suffix: -Beta installer_desktop_icon: true installer_start_menu: true installer_add_path: false diff --git a/build_nuitka.py b/build_nuitka.py index e91ad78..81a38d9 100644 --- a/build_nuitka.py +++ b/build_nuitka.py @@ -14,105 +14,101 @@ # ANSI 颜色代码 class Color: - RESET = "\033[0m" - BOLD = "\033[1m" - GREEN = "\033[92m" - YELLOW = "\033[93m" - RED = "\033[91m" - CYAN = "\033[96m" - GRAY = "\033[90m" + RESET = '\033[0m' + BOLD = '\033[1m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + CYAN = '\033[96m' + GRAY = '\033[90m' # 构建配置 -PROJECT_NAME = "PyBuilder" -VERSION = "1.0.0" -ENTRY_FILE = "main.py" -COMPANY_NAME = "ASLant" -ICON_FILE = "assets/app.ico" -OUTPUT_DIR = "build" - +PROJECT_NAME = 'PyBuilder' +VERSION = '1.0.0' +ENTRY_FILE = 'main.py' +COMPANY_NAME = 'ASLant' +ICON_FILE = 'assets/app.ico' +OUTPUT_DIR = 'build' def build(): """执行 Nuitka 构建""" # 获取平台信息 os_type = platform.system() - is_windows = os_type == "Windows" - is_macos = os_type == "Darwin" - is_linux = os_type == "Linux" - + is_windows = os_type == 'Windows' + is_macos = os_type == 'Darwin' + is_linux = os_type == 'Linux' + # 获取终端宽度 width = shutil.get_terminal_size().columns - separator = "-" * width + separator = '-' * width start_time = time.time() - print( - f"{Color.CYAN}{Color.BOLD}Building {PROJECT_NAME} v{VERSION} on {os_type}{Color.RESET}" - ) + print(f'{Color.CYAN}{Color.BOLD}Building {PROJECT_NAME} v{VERSION} on {os_type}{Color.RESET}') print(separator) # 构建 Nuitka 命令 cmd = [ sys.executable, - "-m", - "nuitka", - "--mode=standalone", - f"--output-dir={OUTPUT_DIR}", - f"--output-filename={PROJECT_NAME}", - f"--output-folder-name={PROJECT_NAME}.dist", - "--lto=auto", - "--jobs=16", - "--python-flag=-O", - "--quiet", - "--remove-output", - "--include-package=pygments", - f"--include-data-dir={os.path.join('src', 'style')}={os.path.join('src', 'style')}", - f"--include-data-dir={'docs'}={'docs'}", + '-m', 'nuitka', + '--mode=standalone', + f'--output-dir={OUTPUT_DIR}', + f'--output-filename={PROJECT_NAME}', + f'--output-folder-name={PROJECT_NAME}.dist', + '--python-flag=-O', + '--quiet', + '--remove-output', + '--follow-imports', + '--assume-yes-for-downloads', + f'--include-data-dir={os.path.join('src', 'style')}={os.path.join('src', 'style')}', + f'--include-data-dir={os.path.join('assets', 'pyfiglet')}={'pyfiglet'}', + f'--include-data-dir={'docs'}={'docs'}', ] # Windows图标(仅Windows平台) if is_windows: - cmd.append(f"--windows-icon-from-ico={ICON_FILE}") + cmd.append(f'--windows-icon-from-ico={ICON_FILE}') # Windows公司名称(仅Windows平台) if is_windows: - cmd.append(f"--windows-company-name={COMPANY_NAME}") + cmd.append(f'--windows-company-name={COMPANY_NAME}') # Windows版本信息(仅Windows平台) if is_windows: - cmd.append(f"--windows-product-version={VERSION}") - cmd.append(f"--windows-file-version={VERSION}") + cmd.append(f'--windows-product-version={VERSION}') + cmd.append(f'--windows-file-version={VERSION}') # 根据平台选择编译器 - compiler = "msvc" + compiler = 'clang' if is_windows: # Windows平台编译器 - if compiler == "clang": - cmd.append("--clang") - elif compiler == "mingw64": - cmd.append("--mingw64") - elif compiler == "clang-cl": - cmd.append("--clang-cl") + if compiler == 'clang': + cmd.append('--clang') + elif compiler == 'mingw64': + cmd.append('--mingw64') + elif compiler == 'clang-cl': + cmd.append('--clang-cl') # msvc是默认,不需要参数 elif is_linux: # Linux平台编译器 - if compiler == "clang": - cmd.append("--clang") + if compiler == 'clang': + cmd.append('--clang') # gcc是默认,不需要参数 elif is_macos: # macOS平台编译器 - if compiler != "clang": + if compiler != 'clang': # clang是macOS默认,其他需要指定 - if compiler == "gcc": - print(f"{Color.YELLOW}注意: macOS推荐使用Clang{Color.RESET}") + if compiler == 'gcc': + print(f'{Color.YELLOW}注意: macOS推荐使用Clang{Color.RESET}') # 添加入口文件 cmd.append(ENTRY_FILE) # 执行构建 - print(f"{Color.GRAY}Command:{Color.RESET}") - print(f"{Color.GRAY}" + " ".join(cmd) + f"{Color.RESET}") + print(f'{Color.GRAY}Command:{Color.RESET}') + print(f'{Color.GRAY}' + ' '.join(cmd) + f'{Color.RESET}') print(separator) - print(f"{Color.YELLOW}Building, please wait...{Color.RESET}") + print(f'{Color.YELLOW}Building, please wait...{Color.RESET}') print() try: @@ -121,25 +117,23 @@ def build(): elapsed_time = time.time() - start_time minutes = int(elapsed_time // 60) seconds = int(elapsed_time % 60) - print(f"{Color.GREEN}{Color.BOLD}Build successful!{Color.RESET}") + print(f'{Color.GREEN}{Color.BOLD}Build successful!{Color.RESET}') abs_output = os.path.abspath(OUTPUT_DIR) - print(f"{Color.GREEN}Output: {abs_output}{Color.RESET}") + print(f'{Color.GREEN}Output: {abs_output}{Color.RESET}') if minutes > 0: - print(f"{Color.CYAN}Build time: {minutes}m {seconds}s{Color.RESET}") + print(f'{Color.CYAN}Build time: {minutes}m {seconds}s{Color.RESET}') else: - print(f"{Color.CYAN}Build time: {seconds}s{Color.RESET}") + print(f'{Color.CYAN}Build time: {seconds}s{Color.RESET}') return 0 except subprocess.CalledProcessError as e: print(separator) - print( - f"{Color.RED}{Color.BOLD}Build failed: {Color.RESET}{Color.RED}{e}{Color.RESET}" - ) + print(f'{Color.RED}{Color.BOLD}Build failed: {Color.RESET}{Color.RED}{e}{Color.RESET}') return 1 except Exception as e: print(separator) - print(f"{Color.RED}{Color.BOLD}Error: {Color.RESET}{Color.RED}{e}{Color.RESET}") + print(f'{Color.RED}{Color.BOLD}Error: {Color.RESET}{Color.RED}{e}{Color.RESET}') return 1 -if __name__ == "__main__": +if __name__ == '__main__': sys.exit(build()) diff --git a/build_pyinstaller.py b/build_pyinstaller.py index 77434af..843f2a4 100644 --- a/build_pyinstaller.py +++ b/build_pyinstaller.py @@ -38,7 +38,6 @@ def build(): os_type = platform.system() is_windows = os_type == "Windows" is_macos = os_type == "Darwin" - is_linux = os_type == "Linux" # 获取终端宽度 width = shutil.get_terminal_size().columns diff --git a/msvc_build_nuitka.py b/msvc_build_nuitka.py new file mode 100644 index 0000000..e91ad78 --- /dev/null +++ b/msvc_build_nuitka.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +""" +PyBuilder - Nuitka 构建脚本 +版本: 1.0.0 +""" + +import sys +import os +import subprocess +import shutil +import time +import platform + + +# ANSI 颜色代码 +class Color: + RESET = "\033[0m" + BOLD = "\033[1m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + RED = "\033[91m" + CYAN = "\033[96m" + GRAY = "\033[90m" + + +# 构建配置 +PROJECT_NAME = "PyBuilder" +VERSION = "1.0.0" +ENTRY_FILE = "main.py" +COMPANY_NAME = "ASLant" +ICON_FILE = "assets/app.ico" +OUTPUT_DIR = "build" + + +def build(): + """执行 Nuitka 构建""" + # 获取平台信息 + os_type = platform.system() + is_windows = os_type == "Windows" + is_macos = os_type == "Darwin" + is_linux = os_type == "Linux" + + # 获取终端宽度 + width = shutil.get_terminal_size().columns + separator = "-" * width + start_time = time.time() + + print( + f"{Color.CYAN}{Color.BOLD}Building {PROJECT_NAME} v{VERSION} on {os_type}{Color.RESET}" + ) + print(separator) + + # 构建 Nuitka 命令 + cmd = [ + sys.executable, + "-m", + "nuitka", + "--mode=standalone", + f"--output-dir={OUTPUT_DIR}", + f"--output-filename={PROJECT_NAME}", + f"--output-folder-name={PROJECT_NAME}.dist", + "--lto=auto", + "--jobs=16", + "--python-flag=-O", + "--quiet", + "--remove-output", + "--include-package=pygments", + f"--include-data-dir={os.path.join('src', 'style')}={os.path.join('src', 'style')}", + f"--include-data-dir={'docs'}={'docs'}", + ] + + # Windows图标(仅Windows平台) + if is_windows: + cmd.append(f"--windows-icon-from-ico={ICON_FILE}") + + # Windows公司名称(仅Windows平台) + if is_windows: + cmd.append(f"--windows-company-name={COMPANY_NAME}") + + # Windows版本信息(仅Windows平台) + if is_windows: + cmd.append(f"--windows-product-version={VERSION}") + cmd.append(f"--windows-file-version={VERSION}") + + # 根据平台选择编译器 + compiler = "msvc" + if is_windows: + # Windows平台编译器 + if compiler == "clang": + cmd.append("--clang") + elif compiler == "mingw64": + cmd.append("--mingw64") + elif compiler == "clang-cl": + cmd.append("--clang-cl") + # msvc是默认,不需要参数 + elif is_linux: + # Linux平台编译器 + if compiler == "clang": + cmd.append("--clang") + # gcc是默认,不需要参数 + elif is_macos: + # macOS平台编译器 + if compiler != "clang": + # clang是macOS默认,其他需要指定 + if compiler == "gcc": + print(f"{Color.YELLOW}注意: macOS推荐使用Clang{Color.RESET}") + + # 添加入口文件 + cmd.append(ENTRY_FILE) + + # 执行构建 + print(f"{Color.GRAY}Command:{Color.RESET}") + print(f"{Color.GRAY}" + " ".join(cmd) + f"{Color.RESET}") + print(separator) + print(f"{Color.YELLOW}Building, please wait...{Color.RESET}") + print() + + try: + subprocess.run(cmd, check=True) + print(separator) + elapsed_time = time.time() - start_time + minutes = int(elapsed_time // 60) + seconds = int(elapsed_time % 60) + print(f"{Color.GREEN}{Color.BOLD}Build successful!{Color.RESET}") + abs_output = os.path.abspath(OUTPUT_DIR) + print(f"{Color.GREEN}Output: {abs_output}{Color.RESET}") + if minutes > 0: + print(f"{Color.CYAN}Build time: {minutes}m {seconds}s{Color.RESET}") + else: + print(f"{Color.CYAN}Build time: {seconds}s{Color.RESET}") + return 0 + except subprocess.CalledProcessError as e: + print(separator) + print( + f"{Color.RED}{Color.BOLD}Build failed: {Color.RESET}{Color.RED}{e}{Color.RESET}" + ) + return 1 + except Exception as e: + print(separator) + print(f"{Color.RED}{Color.BOLD}Error: {Color.RESET}{Color.RED}{e}{Color.RESET}") + return 1 + + +if __name__ == "__main__": + sys.exit(build()) diff --git a/src/app.py b/src/app.py index 896145f..ee7f47c 100644 --- a/src/app.py +++ b/src/app.py @@ -3,6 +3,8 @@ """ import asyncio +from pathlib import Path +from typing import Literal from textual.app import App from textual.binding import Binding from textual import events @@ -11,6 +13,9 @@ from src.utils import load_config, save_config +SeverityLevel = Literal["information", "warning", "error"] + + class PyBuildTUI(App): """Python 构建脚本生成器 TUI 应用""" @@ -55,8 +60,8 @@ def __init__(self): super().__init__() # 项目配置 - self.project_dir = None # 选中的项目目录 - self.build_mode = None # 构建模式: simple, full, expert + self.project_dir: Path | None = None # 选中的项目目录 + self.build_mode: str | None = None # 构建模式: simple, full, expert # 加载配置 self.config = load_config() @@ -73,7 +78,7 @@ def notify( message: str, *, title: str = "", - severity: str = "information", + severity: SeverityLevel = "information", timeout: float | None = None, markup: bool = True, ) -> None: diff --git a/src/screens/compile_config_screen.py b/src/screens/compile_config_screen.py index 99ca9ba..e637ba8 100644 --- a/src/screens/compile_config_screen.py +++ b/src/screens/compile_config_screen.py @@ -32,7 +32,7 @@ class CompileConfigScreen(Screen): def __init__(self): super().__init__() self.config = {} - self.project_dir: Path = None + self.project_dir: Path | None = None def compose(self) -> ComposeResult: """创建界面组件""" @@ -121,7 +121,7 @@ def compose(self) -> ComposeResult: async def on_mount(self) -> None: """挂载时加载配置""" - self.project_dir = self.app.project_dir + self.project_dir = self.app.project_dir # type: ignore[assignment] if not self.project_dir: self.app.notify("未选择项目目录", severity="error") self.app.pop_screen() @@ -160,7 +160,7 @@ def _load_config_to_ui(self) -> None: def _save_config_from_ui(self) -> None: """从UI保存配置(只更新编译配置字段,保留打包选项)""" # 先加载现有配置,保留打包选项字段 - existing_config = load_build_config(self.project_dir) + existing_config = load_build_config(self.project_dir) # type: ignore[arg-type] # 只更新编译配置相关的字段 existing_config["project_name"] = self.query_one( @@ -206,7 +206,7 @@ def _validate_and_save(self) -> bool: self._save_config_from_ui() # 验证配置 - is_valid, error_msg = validate_build_config(self.config, self.project_dir) + is_valid, error_msg = validate_build_config(self.config, self.project_dir) # type: ignore[arg-type] if not is_valid: self.app.notify(f"配置验证失败: {error_msg}", severity="error") return False @@ -215,7 +215,7 @@ def _validate_and_save(self) -> bool: async def _async_save_config(self) -> bool: """异步保存配置到文件""" - success = await async_save_build_config(self.project_dir, self.config) + success = await async_save_build_config(self.project_dir, self.config) # type: ignore[arg-type] if not success: self.app.notify("配置保存失败", severity="error") return success diff --git a/src/screens/compiler_selector_screen.py b/src/screens/compiler_selector_screen.py index f82500e..e0b9014 100644 --- a/src/screens/compiler_selector_screen.py +++ b/src/screens/compiler_selector_screen.py @@ -47,7 +47,7 @@ class CompilerSelectorScreen(Screen): Binding("enter", "confirm", "确认"), ] - def __init__(self, selected_compiler: str = None): + def __init__(self, selected_compiler: str | None = None): super().__init__() self.os_type = platform.system() # 根据平台设置默认编译器 diff --git a/src/screens/installer_config_screen.py b/src/screens/installer_config_screen.py index 5ea43a9..58a9529 100644 --- a/src/screens/installer_config_screen.py +++ b/src/screens/installer_config_screen.py @@ -31,7 +31,7 @@ class InstallerConfigScreen(Screen): def __init__(self): super().__init__() self.config = {} - self.project_dir: Path = None + self.project_dir: Path | None = None def compose(self) -> ComposeResult: """创建界面组件""" @@ -121,7 +121,7 @@ def compose(self) -> ComposeResult: async def on_mount(self) -> None: """挂载时加载配置""" - self.project_dir = self.app.project_dir + self.project_dir = self.app.project_dir # type: ignore[assignment] if not self.project_dir: self.app.notify("未选择项目目录", severity="error") self.app.pop_screen() @@ -164,7 +164,7 @@ def _load_config_to_ui(self) -> None: def _save_config_from_ui(self) -> None: """从UI保存配置""" - existing_config = load_build_config(self.project_dir) + existing_config = load_build_config(self.project_dir) # type: ignore[arg-type] existing_config["installer_platform"] = self.query_one( "#platform-select", Select @@ -215,7 +215,7 @@ def _validate_and_save(self) -> bool: async def _async_save_config(self) -> bool: """异步保存配置到文件""" - success = await async_save_build_config(self.project_dir, self.config) + success = await async_save_build_config(self.project_dir, self.config) # type: ignore[arg-type] if not success: self.app.notify("配置保存失败", severity="error") return success @@ -225,7 +225,9 @@ def on_select_changed(self, event: Select.Changed) -> None: if event.select.id == "platform-select": if event.value == "macos": self.app.notify("macOS 打包功能开发中...", severity="warning") - event.select.value = "windows" + # 重置为 windows(需要类型断言) + select_widget = event.select + select_widget.value = "windows" # type: ignore[arg-type] def on_button_pressed(self, event: Button.Pressed) -> None: """处理按钮点击""" diff --git a/src/screens/installer_options_screen.py b/src/screens/installer_options_screen.py index 5f82994..c33b160 100644 --- a/src/screens/installer_options_screen.py +++ b/src/screens/installer_options_screen.py @@ -40,7 +40,7 @@ class InstallerOptionsScreen(Screen): def __init__(self): super().__init__() self.config = {} - self.project_dir: Path = None + self.project_dir: Path | None = None def compose(self) -> ComposeResult: """创建界面组件""" @@ -72,7 +72,7 @@ def compose(self) -> ComposeResult: def on_mount(self) -> None: """挂载时加载配置""" - self.project_dir = self.app.project_dir + self.project_dir = self.app.project_dir # type: ignore[assignment] if not self.project_dir: self.app.notify("未选择项目目录", severity="error") self.app.pop_screen() @@ -83,7 +83,7 @@ def on_mount(self) -> None: async def _load_and_create_fields(self) -> None: """加载配置并创建字段""" try: - self.config = await async_load_build_config(self.project_dir) + self.config = await async_load_build_config(self.project_dir) # type: ignore[arg-type] self._create_options_fields() except Exception as e: self._show_load_error(str(e)) @@ -226,31 +226,32 @@ def _create_options_fields(self) -> None: ) # ===== 高级选项标签页 ===== + # 自定义后缀 + advanced_row0 = self._create_input_widget( + "custom-suffix-input", + "安装包自定义后缀 (可留空):", + "例如: @ASLant (生成 xxx_setup@ASLant.exe)", + self.config.get("installer_custom_suffix", ""), + ) + # 关联文件类型 - advanced_row1 = Horizontal( - self._create_input_widget( - "file-assoc-input", - "关联文件类型:", - "例如: .txt,.log,.cfg", - self.config.get("installer_file_assoc", ""), - ), - Vertical(classes="field-group"), # 占位 - classes="inputs-row", + advanced_row1 = self._create_input_widget( + "file-assoc-input", + "关联文件类型:", + "例如: .txt,.log,.cfg", + self.config.get("installer_file_assoc", ""), ) # 额外快捷方式 - advanced_row2 = Horizontal( - self._create_input_widget( - "extra-shortcuts-input", - "额外快捷方式 (名称;exe文件):", - "例如: Word;word.exe Excel;excel.exe", - self.config.get("installer_extra_shortcuts", ""), - ), - Vertical(classes="field-group"), # 占位 - classes="inputs-row", + advanced_row2 = self._create_input_widget( + "extra-shortcuts-input", + "额外快捷方式 (名称;exe文件):", + "例如: Word;word.exe Excel;excel.exe", + self.config.get("installer_extra_shortcuts", ""), ) advanced_content = Vertical( + advanced_row0, advanced_row1, advanced_row2, classes="basic-options-content", @@ -351,7 +352,7 @@ def _create_options_fields(self) -> None: def _save_config_from_ui(self) -> None: """从UI保存配置""" - existing_config = load_build_config(self.project_dir) + existing_config = load_build_config(self.project_dir) # type: ignore[arg-type] # 基本选项 existing_config["installer_desktop_icon"] = self.query_one( @@ -377,6 +378,9 @@ def _save_config_from_ui(self) -> None: ).value # 高级选项 + existing_config["installer_custom_suffix"] = self.query_one( + "#custom-suffix-input", Input + ).value.strip() existing_config["installer_file_assoc"] = self.query_one( "#file-assoc-input", Input ).value.strip() @@ -419,7 +423,7 @@ def _validate_and_save(self) -> bool: async def _async_save_config(self) -> bool: """异步保存配置到文件""" - success = await async_save_build_config(self.project_dir, self.config) + success = await async_save_build_config(self.project_dir, self.config) # type: ignore[arg-type] if not success: self.app.notify("配置保存失败", severity="error") return success @@ -464,7 +468,7 @@ async def action_generate(self) -> None: from src.screens.installer_generation_screen import InstallerGenerationScreen result = await self.app.push_screen_wait( - InstallerGenerationScreen(self.config, self.project_dir) + InstallerGenerationScreen(self.config, self.project_dir) # type: ignore[arg-type] ) if result and result[0]: diff --git a/src/screens/mode_selector_screen.py b/src/screens/mode_selector_screen.py index e194f7f..8d7930e 100644 --- a/src/screens/mode_selector_screen.py +++ b/src/screens/mode_selector_screen.py @@ -88,7 +88,7 @@ def action_back(self) -> None: def action_confirm(self) -> None: """确认选择并进入下一步""" - self.app.build_mode = self.selected_mode + self.app.build_mode = self.selected_mode # type: ignore[assignment] mode_names = { "full": "完整模式", diff --git a/src/screens/package_options_screen.py b/src/screens/package_options_screen.py index 36f02b9..29751a8 100644 --- a/src/screens/package_options_screen.py +++ b/src/screens/package_options_screen.py @@ -39,7 +39,7 @@ class PackageOptionsScreen(Screen): def __init__(self): super().__init__() self.config = {} - self.project_dir: Path = None + self.project_dir: Path | None = None self.selected_plugins: list[str] = [] # 存储选中的插件 # 根据平台设置默认编译器 import platform @@ -84,7 +84,7 @@ def compose(self) -> ComposeResult: def on_mount(self) -> None: """挂载时加载配置""" - self.project_dir = self.app.project_dir + self.project_dir = self.app.project_dir # type: ignore[assignment] if not self.project_dir: self.app.notify("未选择项目目录", severity="error") self.app.pop_screen() @@ -97,7 +97,7 @@ async def _load_and_create_fields(self) -> None: """异步加载配置并创建字段""" try: # 异步加载现有配置或使用默认配置 - self.config = await async_load_build_config(self.project_dir) + self.config = await async_load_build_config(self.project_dir) # type: ignore[arg-type] # 加载插件配置 plugins_value = self.config.get("plugins", "") @@ -214,7 +214,7 @@ def _create_options_fields(self) -> None: def _save_config_from_ui(self) -> None: """从UI保存配置到内存(只更新打包选项字段,保留编译配置)""" # 先加载现有配置,保留编译配置字段 - existing_config = load_build_config(self.project_dir) + existing_config = load_build_config(self.project_dir) # type: ignore[arg-type] build_tool = existing_config.get("build_tool", "nuitka") # Nuitka特有选项 @@ -580,7 +580,7 @@ def _validate_and_save(self) -> bool: self._save_config_from_ui() # 验证配置 - is_valid, error_msg = validate_build_config(self.config, self.project_dir) + is_valid, error_msg = validate_build_config(self.config, self.project_dir) # type: ignore[arg-type] if not is_valid: self.app.notify(f"配置验证失败: {error_msg}", severity="error") return False @@ -589,7 +589,7 @@ def _validate_and_save(self) -> bool: async def _async_save_config(self) -> bool: """异步保存配置到文件""" - success = await async_save_build_config(self.project_dir, self.config) + success = await async_save_build_config(self.project_dir, self.config) # type: ignore[arg-type] if not success: self.app.notify("配置保存失败", severity="error") return success @@ -619,7 +619,7 @@ async def action_generate(self) -> None: from src.screens.generation_screen import GenerationScreen result = await self.app.push_screen_wait( - GenerationScreen(self.config, self.project_dir) + GenerationScreen(self.config, self.project_dir) # type: ignore[arg-type] ) if result: diff --git a/src/screens/plugin_selector_screen.py b/src/screens/plugin_selector_screen.py index 463ff8d..cfde80c 100644 --- a/src/screens/plugin_selector_screen.py +++ b/src/screens/plugin_selector_screen.py @@ -21,7 +21,7 @@ class PluginSelectorScreen(Screen): Binding("enter", "confirm", "确认"), ] - def __init__(self, selected_plugins: list[str] = None): + def __init__(self, selected_plugins: list[str] | None = None): super().__init__() self.selected_plugins = selected_plugins or [] diff --git a/src/screens/project_selector_screen.py b/src/screens/project_selector_screen.py index 6a33e2b..ef147ad 100644 --- a/src/screens/project_selector_screen.py +++ b/src/screens/project_selector_screen.py @@ -127,7 +127,7 @@ def _update_list_view(self, dir_items: List[Path]) -> None: parent = self.selected_path.parent if parent != self.selected_path: parent_item = ListItem(Label("📁 .."), classes="parent-dir") - parent_item.is_parent = True + parent_item.is_parent = True # type: ignore[attr-defined] all_items.append(parent_item) # 添加目录项(限制数量) @@ -137,13 +137,13 @@ def _update_list_view(self, dir_items: List[Path]) -> None: icon = "📁" label = Label(f"{icon} {item.name}") list_item = ListItem(label, classes="directory") - list_item.item_path = item + list_item.item_path = item # type: ignore[attr-defined] all_items.append(list_item) else: icon = "📄" label = Label(f"{icon} {item.name}") list_item = ListItem(label, classes="file") - list_item.item_path = item + list_item.item_path = item # type: ignore[attr-defined] all_items.append(list_item) # 添加到 ListView @@ -153,18 +153,18 @@ def _update_list_view(self, dir_items: List[Path]) -> None: def on_list_view_selected(self, event: ListView.Selected) -> None: """列表项选择事件""" # 检查是否是父目录项 - if hasattr(event.item, "is_parent") and event.item.is_parent: + if hasattr(event.item, "is_parent") and event.item.is_parent: # type: ignore[attr-defined] # 点击 ".." 返回上一级 parent_path = self.selected_path.parent if parent_path != self.selected_path: self.selected_path = parent_path self.update_selected_path() self.refresh_directory_list_async() - elif hasattr(event.item, "item_path"): - item_path = event.item.item_path - if item_path.is_dir(): + elif hasattr(event.item, "item_path"): # type: ignore[attr-defined] + item_path = event.item.item_path # type: ignore[attr-defined] + if item_path.is_dir(): # type: ignore[attr-defined] # 点击文件夹,进入该目录 - self.selected_path = item_path + self.selected_path = item_path # type: ignore[assignment] self.update_selected_path() self.refresh_directory_list_async() # 文件不做处理 @@ -215,7 +215,7 @@ def action_confirm(self) -> None: return # 保存选中的项目路径到 app - self.app.project_dir = self.selected_path + self.app.project_dir = self.selected_path # type: ignore[assignment] # 跳转到模式选择屏幕 from src.screens.mode_selector_screen import ModeSelectorScreen diff --git a/src/style/compile_config_screen.tcss b/src/style/compile_config_screen.tcss index 2d9beda..8164348 100644 --- a/src/style/compile_config_screen.tcss +++ b/src/style/compile_config_screen.tcss @@ -90,7 +90,7 @@ CompileConfigScreen, InstallerConfigScreen { dock: bottom; layout: horizontal; align: center middle; - margin-top: 1; + padding: 1 0; } Button { diff --git a/src/style/package_options_screen.tcss b/src/style/package_options_screen.tcss index 1d917ca..751be5a 100644 --- a/src/style/package_options_screen.tcss +++ b/src/style/package_options_screen.tcss @@ -135,7 +135,7 @@ ToastRack { dock: bottom; layout: horizontal; align: center middle; - margin-top: 1; + padding: 1 0; } Button { diff --git a/src/utils/build_config.py b/src/utils/build_config.py index 3f4f179..80338c3 100644 --- a/src/utils/build_config.py +++ b/src/utils/build_config.py @@ -305,6 +305,7 @@ def save_build_config(project_dir: Path, config: Dict[str, Any]) -> bool: ("installer_privileges", "安装权限"), ("installer_compression", "压缩方式"), ("installer_path_scope", "PATH作用域"), + ("installer_custom_suffix", "安装包自定义后缀"), ("installer_file_assoc", "关联文件类型"), ("installer_extra_shortcuts", "额外快捷方式"), ] diff --git a/src/utils/installer_generator.py b/src/utils/installer_generator.py index 1d14a37..3d1faac 100644 --- a/src/utils/installer_generator.py +++ b/src/utils/installer_generator.py @@ -52,6 +52,9 @@ def generate_inno_setup_script(config: Dict[str, Any], project_dir: Path) -> str license_file = config.get("installer_license", "") readme_file = config.get("installer_readme", "") + # 自定义后缀(用于安装包文件名) + custom_suffix = config.get("installer_custom_suffix", "").strip() + # AppId(不带花括号存储) app_id = config.get("installer_appid", "").strip().strip("{}") if not app_id: @@ -99,7 +102,9 @@ def generate_inno_setup_script(config: Dict[str, Any], project_dir: Path) -> str # 输出设置 lines.append("; 输出设置") lines.append(f"OutputDir={output_dir}") - lines.append("OutputBaseFilename={#MyAppName}_V{#MyAppVersion}_Setup") + # 构建输出文件名:支持自定义后缀 + output_filename = f"{{#MyAppName}}_V{{#MyAppVersion}}_Setup{custom_suffix}" + lines.append(f"OutputBaseFilename={output_filename}") if icon_file: lines.append(f"SetupIconFile={icon_file}") lines.append("") diff --git a/src/utils/script_generator.py b/src/utils/script_generator.py index b354773..47d5e45 100644 --- a/src/utils/script_generator.py +++ b/src/utils/script_generator.py @@ -57,7 +57,7 @@ def _generate_script_header(config: Dict[str, Any], tool_name: str) -> List[str] def _generate_config_section( - config: Dict[str, Any], extra_vars: List[str] = None + config: Dict[str, Any], extra_vars: List[str] | None = None ) -> List[str]: """生成配置常量部分""" lines = [ @@ -112,7 +112,7 @@ def _generate_build_execution() -> List[str]: ] -def _generate_build_result(cleanup_code: List[str] = None) -> List[str]: +def _generate_build_result(cleanup_code: List[str] | None = None) -> List[str]: """生成构建结果处理部分""" lines = [ " try:", @@ -377,7 +377,7 @@ def generate_pyinstaller_script(config: Dict[str, Any], project_dir: Path) -> st lines.extend(_generate_script_header(config, "PyInstaller")) # PyInstaller 特有的额外变量 - extra_vars = [] + extra_vars: List[str] = [] if config.get("splash_image"): extra_vars.append(f"SPLASH_IMAGE = '{config['splash_image']}'") lines.extend(_generate_config_section(config, extra_vars if extra_vars else None))