#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 把 src/main.py 打包成单文件 exe(工具名: EDID-Viewer) ================================================ 与 DLPilot/src/build_exe.py 同一套路: onefile + --noconsole, Python 解释器、 PySide6 和 PySide6-Fluent-Widgets 全部打进一个 exe, 目标机器无需安装依赖。 用法: python build_exe.py 自动安装 PyInstaller(如缺)并打包 python build_exe.py --debug 额外打一个带控制台的变体 产物: release/EDID-Viewer.exe spec 由本脚本生成到 src/build/(不入库), 调整打包逻辑直接改这里的模板。 """ import os import subprocess import sys # 本脚本位于 src/, 打包产物输出到工具根的 release/ SRC = os.path.dirname(os.path.abspath(__file__)) BASE = os.path.dirname(SRC) RELEASE = os.path.join(BASE, 'release') WORK = os.path.join(SRC, 'build') ICO = os.path.join(BASE, 'assets', 'edid-viewer.ico') SPEC_TEMPLATE = """\ # -*- coding: utf-8 -*- # 由 build_exe.py 生成, 不要手改; 调整打包逻辑请改 src/build_exe.py。 from PyInstaller.utils.hooks import collect_all qfluent_datas, qfluent_binaries, qfluent_hidden = collect_all('qfluentwidgets') a = Analysis( [r'{entry}'], pathex=[r'{src}'], binaries=qfluent_binaries, datas=qfluent_datas + {icon_datas}, hiddenimports=qfluent_hidden, noarchive=False) # Qt 6.11 使用 Windows 非版本化 ICU 导出; PATH 上 Poppler/Conda 的版本化 # ICU(icuuc.dll/icudt78.dll)会被 PyInstaller 误收, 令 QtCore.pyd 启动报 # WinError 127 —— 打包时剔除, 运行时用系统自带 ICU。 a.binaries = [b for b in a.binaries if b[0].lower() not in ('icuuc.dll', 'icudt78.dll')] pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, a.binaries, a.datas, [], name='{name}', debug=False, strip=False, upx=False, console={console}{icon_clause}) """ def ensure_pyinstaller(): try: import PyInstaller # noqa: F401 return except ImportError: pass print('PyInstaller 未安装, 正在安装...') subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pyinstaller']) 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, console): remove_stale(os.path.join(RELEASE, name + '.exe')) if os.path.exists(ICO): icon_datas = f"[(r'{ICO}', 'assets')]" icon_clause = f", icon=r'{ICO}'" else: print('警告: 未找到 assets/edid-viewer.ico(先运行 assets/make_icon.py), 本次打包不带图标') icon_datas, icon_clause = '[]', '' spec = SPEC_TEMPLATE.format(entry=os.path.join(SRC, 'main.py'), src=SRC, icon_datas=icon_datas, name=name, console=console, icon_clause=icon_clause) os.makedirs(WORK, exist_ok=True) spec_path = os.path.join(WORK, name + '.spec') with open(spec_path, 'w', encoding='utf-8') as f: f.write(spec) cmd = [sys.executable, '-m', 'PyInstaller', '--noconfirm', '--clean', '--distpath', RELEASE, '--workpath', WORK, spec_path] print('\n>>', ' '.join(cmd)) subprocess.check_call(cmd) out = os.path.join(RELEASE, name + '.exe') print(f'完成: {out} ({os.path.getsize(out) / 1024 / 1024:.1f} MB)') def main(): build_console = '--debug' in sys.argv ensure_pyinstaller() # 常规版: 双击即用, 无黑框 build('EDID-Viewer', 'False') if build_console: # 调试版: 带控制台, 异常栈可见 build('EDID-Viewer-console', 'True') if __name__ == '__main__': main()