Files
feiqiu-ETL/etl_billiards/build_exe.py
2026-01-27 23:45:36 +08:00

258 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()