# -*- coding: utf-8 -*- """ ETL GUI 打包脚本 使用 PyInstaller 将 GUI 应用打包为 Windows EXE 用法: python build_exe.py [--onefile] [--console] [--clean] 参数: --onefile 打包为单个 EXE 文件(默认为目录模式) --console 显示控制台窗口(调试用) --clean 打包前清理旧的构建文件 """ import os import sys import shutil import subprocess from pathlib import Path def get_project_root() -> Path: """获取项目根目录""" return Path(__file__).resolve().parent def clean_build(): """清理旧的构建文件""" project_root = get_project_root() dirs_to_clean = [ project_root / "build", project_root / "dist", ] files_to_clean = [ project_root / "etl_gui.spec", ] for d in dirs_to_clean: if d.exists(): print(f"清理目录: {d}") shutil.rmtree(d) for f in files_to_clean: if f.exists(): print(f"清理文件: {f}") f.unlink() def build_exe(onefile: bool = False, console: bool = False): """构建 EXE""" project_root = get_project_root() # 主入口 main_script = project_root / "gui" / "main.py" # 资源文件 resources_dir = project_root / "gui" / "resources" database_dir = project_root / "database" # 构建 PyInstaller 命令 # 使用 ASCII 名称避免 Windows 控制台编码问题 cmd = [ sys.executable, "-m", "PyInstaller", "--name", "ETL_Manager", "--noconfirm", ] # 单文件或目录模式 if onefile: cmd.append("--onefile") else: cmd.append("--onedir") # 窗口模式 if not console: cmd.append("--windowed") # 添加数据文件 # 样式表 if resources_dir.exists(): cmd.extend(["--add-data", f"{resources_dir};gui/resources"]) # 数据库 SQL 文件 if database_dir.exists(): for sql_file in database_dir.glob("*.sql"): cmd.extend(["--add-data", f"{sql_file};database"]) # 收集完整模块(解决跨机器运行时缺少模块的问题) collect_all_modules = [ "shiboken6", # PySide6 依赖,必须完整打包 "PySide6", # Qt 绑定,必须完整打包 ] for mod in collect_all_modules: cmd.extend(["--collect-all", mod]) # 隐式导入 hidden_imports = [ # shiboken6 核心模块(解决 No module named 'shiboken6.Shiboken' 错误) "shiboken6", "shiboken6.Shiboken", # PySide6 核心模块 "PySide6.QtCore", "PySide6.QtGui", "PySide6.QtWidgets", "PySide6.QtNetwork", # 数据库 "psycopg2", "psycopg2.extras", "psycopg2.extensions", # GUI 模块 "gui.models.task_model", "gui.models.schedule_model", "gui.utils.cli_builder", "gui.utils.config_helper", "gui.utils.app_settings", "gui.workers.task_worker", "gui.workers.db_worker", "gui.widgets.settings_dialog", ] for imp in hidden_imports: cmd.extend(["--hidden-import", imp]) # 排除不需要的模块(减小体积) excludes = [ "matplotlib", "numpy", "pandas", "scipy", "PIL", "cv2", "tkinter", ] for exc in excludes: cmd.extend(["--exclude-module", exc]) # 工作目录 cmd.extend(["--workpath", str(project_root / "build")]) cmd.extend(["--distpath", str(project_root / "dist")]) cmd.extend(["--specpath", str(project_root)]) # 主脚本 cmd.append(str(main_script)) print("执行命令:") print(" ".join(cmd)) print() # 执行打包 result = subprocess.run(cmd, cwd=str(project_root)) if result.returncode == 0: print() print("=" * 50) print("打包成功!") print(f"输出目录: {project_root / 'dist'}") print("=" * 50) else: print() print("打包失败,请检查错误信息") sys.exit(1) def main(): """主函数""" import argparse parser = argparse.ArgumentParser(description="ETL GUI 打包工具") parser.add_argument("--onefile", action="store_true", help="打包为单个 EXE") parser.add_argument("--console", action="store_true", help="显示控制台窗口") parser.add_argument("--clean", action="store_true", help="打包前清理") args = parser.parse_args() # 检查 PyInstaller try: import PyInstaller print(f"PyInstaller 版本: {PyInstaller.__version__}") except ImportError: print("错误: 未安装 PyInstaller") print("请运行: pip install pyinstaller") sys.exit(1) # 清理 if args.clean: clean_build() # 构建 build_exe(onefile=args.onefile, console=args.console) if __name__ == "__main__": main()