104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
把 hdmi_gui.py 打包成单文件 exe(工具名: DLPilot)
|
|
================================
|
|
使用 PyInstaller --onefile --noconsole: Python 解释器、PySide6 和
|
|
PySide6-Fluent-Widgets 全部打进一个 exe, 目标机器无需安装依赖。
|
|
|
|
用法:
|
|
python build_exe.py 自动安装 PyInstaller(如缺)并打包
|
|
python build_exe.py --debug 额外打一个带控制台的变体(--dump 可用)
|
|
|
|
产物: release/DLPilot.exe(及 --debug 时的 DLPilot-console.exe)
|
|
首次运行 onefile exe 会先解压到临时目录, 启动比脚本慢一两秒, 属正常现象。
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
# 本脚本位于 src/, 打包产物输出到工具根的 release/(与 pattern_slots.json 同目录)
|
|
SRC = os.path.dirname(os.path.abspath(__file__))
|
|
BASE = os.path.dirname(SRC)
|
|
ICO = os.path.join(SRC, 'logos', 'logo_dlpilot.ico')
|
|
|
|
|
|
def ensure_pyinstaller():
|
|
try:
|
|
import PyInstaller # noqa: F401
|
|
return
|
|
except ImportError:
|
|
pass
|
|
print('PyInstaller 未安装, 正在安装...')
|
|
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pyinstaller'])
|
|
|
|
|
|
def extra_collect_args():
|
|
"""Fluent Widgets 的主题资源必须收集; PyInstaller 自带 Qt hooks 会处理 PySide6。"""
|
|
try:
|
|
import qfluentwidgets # noqa: F401
|
|
except ImportError:
|
|
print('警告: 未安装 PySide6-Fluent-Widgets, 请先执行 pip install PySide6-Fluent-Widgets')
|
|
return []
|
|
return ['--collect-all', 'qfluentwidgets']
|
|
|
|
|
|
def qt_icu_args():
|
|
"""Qt6Core imports the unversioned Windows ICU API. PyInstaller can
|
|
otherwise pick Poppler's ICU 78 from PATH, whose suffixed exports make
|
|
QtCore.pyd fail with WinError 127 on startup."""
|
|
if os.name == 'nt' and os.path.exists(r'C:\Windows\System32\icuuc.dll'):
|
|
return ['--add-binary', r'C:\Windows\System32\icuuc.dll;.']
|
|
return []
|
|
|
|
|
|
def remove_stale(exe_path):
|
|
"""删除旧 exe; 若被占用(上次验证的进程还在), 先结束同名进程再重试"""
|
|
if not os.path.exists(exe_path):
|
|
return
|
|
try:
|
|
os.remove(exe_path)
|
|
return
|
|
except PermissionError:
|
|
pass
|
|
import time
|
|
print(f'{exe_path} 被占用, 结束残留进程后重试...')
|
|
subprocess.run(['taskkill', '/F', '/IM', os.path.basename(exe_path)],
|
|
capture_output=True)
|
|
time.sleep(1.0)
|
|
os.remove(exe_path)
|
|
|
|
|
|
def build(name, extra_args):
|
|
dist = os.path.join(BASE, 'release')
|
|
work = os.path.join(SRC, 'build')
|
|
remove_stale(os.path.join(dist, name + '.exe'))
|
|
cmd = [sys.executable, '-m', 'PyInstaller',
|
|
'--onefile', '--clean', '--name', name,
|
|
'--distpath', dist, '--workpath', work, '--specpath', work,
|
|
] + extra_args + extra_collect_args() + qt_icu_args() + [os.path.join(SRC, 'hdmi_gui.py')]
|
|
if os.path.exists(ICO):
|
|
# exe 文件图标 + 运行时窗口图标资源(从 _MEIPASS 读取)
|
|
cmd += ['--icon', ICO, '--add-data', f'{ICO};.']
|
|
else:
|
|
print('警告: 未找到 logos/logo_dlpilot.ico(先运行 logos/make_ico.py), 本次打包不带图标')
|
|
print('\n>>', ' '.join(cmd))
|
|
subprocess.check_call(cmd)
|
|
out = os.path.join(dist, name + '.exe')
|
|
print(f'完成: {out} ({os.path.getsize(out) / 1024 / 1024:.1f} MB)')
|
|
|
|
|
|
def main():
|
|
build_console = '--debug' in sys.argv
|
|
ensure_pyinstaller()
|
|
# 常规版: 双击即用, 无黑框(GUI/图案/自定义栏位全部可用, --dump 除外)
|
|
build('DLPilot', ['--noconsole'])
|
|
if build_console:
|
|
# 调试版: 带控制台, --dump / 异常栈可见
|
|
build('DLPilot-console', [])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|