258 lines
7.1 KiB
Python
258 lines
7.1 KiB
Python
# -*- 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"])
|
||
|
||
# 隐式导入
|
||
hidden_imports = [
|
||
# PySide6 核心模块
|
||
"PySide6.QtCore",
|
||
"PySide6.QtGui",
|
||
"PySide6.QtWidgets",
|
||
# 数据库
|
||
"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:
|
||
# 打包后精简:删除不需要的文件
|
||
slim_dist(project_root / "dist" / "ETL_Manager" / "_internal")
|
||
|
||
print()
|
||
print("=" * 50)
|
||
print("打包成功!")
|
||
print(f"输出目录: {project_root / 'dist'}")
|
||
print("=" * 50)
|
||
else:
|
||
print()
|
||
print("打包失败,请检查错误信息")
|
||
sys.exit(1)
|
||
|
||
|
||
def slim_dist(internal_dir: Path):
|
||
"""精简打包后的文件,删除不需要的内容"""
|
||
if not internal_dir.exists():
|
||
return
|
||
|
||
print()
|
||
print("精简打包文件...")
|
||
|
||
removed_size = 0
|
||
|
||
# 1. 删除不需要的翻译文件(只保留中文和英文)
|
||
translations_dir = internal_dir / "PySide6" / "translations"
|
||
if translations_dir.exists():
|
||
keep_langs = {"zh_CN", "zh_TW", "en"}
|
||
for qm_file in translations_dir.glob("*.qm"):
|
||
# 检查是否是需要保留的语言
|
||
keep = False
|
||
for lang in keep_langs:
|
||
if lang in qm_file.name:
|
||
keep = True
|
||
break
|
||
if not keep:
|
||
size = qm_file.stat().st_size
|
||
qm_file.unlink()
|
||
removed_size += size
|
||
|
||
# 2. 删除 opengl32sw.dll(软件渲染,20MB,通常不需要)
|
||
opengl_sw = internal_dir / "PySide6" / "opengl32sw.dll"
|
||
if opengl_sw.exists():
|
||
size = opengl_sw.stat().st_size
|
||
opengl_sw.unlink()
|
||
removed_size += size
|
||
print(f" 删除: opengl32sw.dll ({size / 1024 / 1024:.1f} MB)")
|
||
|
||
# 3. 删除不需要的 Qt 模块 DLL(如果存在)
|
||
unnecessary_dlls = [
|
||
"Qt6Pdf.dll", # PDF 支持
|
||
"Qt6Qml.dll", # QML 引擎
|
||
"Qt6QmlMeta.dll",
|
||
"Qt6QmlModels.dll",
|
||
"Qt6QmlWorkerScript.dll",
|
||
"Qt6Quick.dll", # Quick UI
|
||
"Qt6VirtualKeyboard.dll", # 虚拟键盘
|
||
]
|
||
pyside6_dir = internal_dir / "PySide6"
|
||
for dll_name in unnecessary_dlls:
|
||
dll_path = pyside6_dir / dll_name
|
||
if dll_path.exists():
|
||
size = dll_path.stat().st_size
|
||
dll_path.unlink()
|
||
removed_size += size
|
||
print(f" 删除: {dll_name} ({size / 1024 / 1024:.1f} MB)")
|
||
|
||
# 4. 删除不需要的插件目录
|
||
unnecessary_plugins = [
|
||
"networkinformation", # 网络信息
|
||
"tls", # TLS 支持(数据库已有)
|
||
]
|
||
plugins_dir = pyside6_dir / "plugins"
|
||
if plugins_dir.exists():
|
||
for plugin_name in unnecessary_plugins:
|
||
plugin_path = plugins_dir / plugin_name
|
||
if plugin_path.exists():
|
||
size = sum(f.stat().st_size for f in plugin_path.rglob("*") if f.is_file())
|
||
shutil.rmtree(plugin_path)
|
||
removed_size += size
|
||
print(f" 删除插件: {plugin_name}")
|
||
|
||
print(f"共节省: {removed_size / 1024 / 1024:.1f} MB")
|
||
|
||
|
||
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()
|