altgraph 0.17.4
bidict 0.23.1
blinker 1.9.0
cffi 1.17.1
click 8.2.1
colorama 0.4.6
dnspython 2.7.0
Flask 3.1.1
flask-cors 6.0.0
Flask-SocketIO 5.5.1
gevent 25.5.1
gevent-websocket 0.10.1
greenlet 3.2.3
h11 0.16.0
itsdangerous 2.2.0
Jinja2 3.1.6
MarkupSafe 3.0.2
Nuitka 2.7.6
ordered-set 4.1.0
packaging 25.0
pefile 2023.2.7
pip 22.0.4
portalocker 3.1.1
pyarmor 9.1.7
pyarmor.cli.core 7.6.7
pycparser 2.22
pyinstaller 6.14.0
pyinstaller-hooks-contrib 2025.4
python-engineio 4.12.2
python-socketio 5.13.0
pywin32 310
pywin32-ctypes 0.2.3
setuptools 58.1.0
simple-websocket 1.1.0
waitress 3.0.2
Werkzeug 3.1.3
wsproto 1.2.0
zope.event 5.0
zope.interface 7.2
zstandard 0.23.0以及打包软件的代码import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import subprocess
import threading
import os
import sys
import shutil
import json
class PackerApp:
def __init__(self, root):
self.root = root
root.title("EXE打包专家 v4.0")
root.geometry("1800x900")
self.running = False
# 初始化变量
self.app_path = tk.StringVar()
self.static_dir = tk.StringVar()
self.data_path = tk.StringVar()
self.icon_path = tk.StringVar()
self.safe_pack = tk.BooleanVar(value=True)
# 创建界面
self.create_widgets()
# DPI适配
self.dpi_scaling()
# 初始化日志系统
self.log_buffer = []
def dpi_scaling(self):
"""处理高DPI显示"""
if sys.platform == "win32":
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except:
pass
def create_widgets(self):
# 主框架
main_frame = ttk.Frame(self.root, padding=15)
main_frame.pack(fill=tk.BOTH, expand=True)
# 输入区域
input_frame = ttk.LabelFrame(main_frame, text="文件选择", padding=10)
input_frame.pack(fill=tk.X, pady=5)
self.create_file_row(input_frame, "主程序(.py):", self.app_path, "选择文件", self.select_app)
self.create_file_row(input_frame, "静态目录:", self.static_dir, "选择目录", self.select_static_dir)
self.create_file_row(input_frame, "数据文件(.json):", self.data_path, "选择文件", self.select_data)
self.create_file_row(input_frame, "程序图标(.ico):", self.icon_path, "选择图标", self.select_icon)
# 设置区域
options_frame = ttk.LabelFrame(main_frame, text="打包选项", padding=10)
options_frame.pack(fill=tk.X, pady=5)
ttk.Checkbutton(options_frame, text="单文件模式", variable=tk.BooleanVar(value=True)).grid(row=0, column=0, padx=5)
ttk.Checkbutton(options_frame, text="无控制台窗口", variable=tk.BooleanVar(value=True)).grid(row=0, column=1, padx=5)
ttk.Checkbutton(options_frame, text="安全混淆打包(防逆向)", variable=self.safe_pack).grid(row=0, column=2, padx=5)
# 操作按钮
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="开始打包", command=self.start_pack).grid(row=0, column=0, padx=5)
ttk.Button(btn_frame, text="清除日志", command=self.clear_log).grid(row=0, column=1, padx=5)
# 日志区域
log_frame = ttk.LabelFrame(main_frame, text="打包日志", padding=10)
log_frame.pack(fill=tk.BOTH, expand=True)
self.log_text = tk.Text(log_frame, wrap=tk.WORD, state=tk.DISABLED, font=('Consolas', 20))
vsb = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
self.log_text.configure(yscrollcommand=vsb.set)
self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
vsb.pack(side=tk.RIGHT, fill=tk.Y)
def create_file_row(self, parent, label, var, btn_text, command):
"""创建统一文件选择行"""
frame = ttk.Frame(parent)
frame.pack(fill=tk.X, pady=10)
ttk.Label(frame, text=label, width=20).pack(side=tk.LEFT)
entry = ttk.Entry(frame, textvariable=var, width=30)
entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
ttk.Button(frame, text=btn_text, command=command).pack(side=tk.LEFT)
def select_app(self):
path = filedialog.askopenfilename(filetypes=[("Python文件", "*.py")])
if path:
self.app_path.set(path)
self.log(f"选择主程序: {path}")
def select_static_dir(self):
path = filedialog.askdirectory()
if path:
self.static_dir.set(path)
self.log(f"选择静态目录: {path}")
def select_data(self):
path = filedialog.askopenfilename(filetypes=[("JSON文件", "*.json")])
if path:
self.data_path.set(path)
self.log(f"选择数据文件: {path}")
def select_icon(self):
path = filedialog.askopenfilename(filetypes=[("图标文件", "*.ico")])
if path:
if self.validate_icon(path):
self.icon_path.set(path)
self.log(f"选择图标文件: {path}")
else:
messagebox.showerror("错误", "无效的ICO文件格式")
def validate_icon(self, path):
"""验证ICO文件有效性"""
try:
with open(path, "rb") as f:
header = f.read(4)
return header == b'\x00\x00\x01\x00'
except:
return False
def validate_inputs(self):
"""输入验证"""
errors = []
required = [
(self.app_path, "主程序文件", os.path.isfile),
(self.static_dir, "静态目录", os.path.isdir),
(self.data_path, "数据文件", os.path.isfile)
]
for var, name, check in required:
path = var.get()
if not path:
errors.append(f"请选择{name}")
elif not check(path):
errors.append(f"{name}路径无效: {path}")
if self.icon_path.get() and not os.path.isfile(self.icon_path.get()):
errors.append("图标文件路径无效")
return errors
def start_pack(self):
if self.running:
return
if errors := self.validate_inputs():
messagebox.showerror("输入错误", "\n".join(errors))
return
self.running = True
self.clear_log()
self.log("开始打包进程...")
threading.Thread(target=self.pack_process, daemon=True).start()
def get_pyarmor_cmd(self, obf_dir, main_py):
try:
import pyarmor
version = getattr(pyarmor, '__version__', '7.0.0')
except ImportError:
version = '7.0.0'
if version.startswith('8') or version >= '8.0.0':
# PyArmor 8.x 用 python -m pyarmor.cli obfuscate
pyarmor_cmd = [
sys.executable, "-m", "pyarmor.cli", "obfuscate",
"--output", obf_dir,
main_py
]
else:
# PyArmor 7.x 直接用 pyarmor 命令行
pyarmor_cmd = [
"pyarmor", "pack",
"-e", f"--dist {obf_dir}",
main_py
]
return pyarmor_cmd
def pack_process(self):
try:
if self.safe_pack.get():
obf_dir = os.path.abspath(os.path.join(os.path.dirname(self.app_path.get())))
main_py = os.path.abspath(self.app_path.get())
pyarmor_cmd = self.get_pyarmor_cmd(obf_dir, main_py)
self.log(f"执行PyArmor混淆: {' '.join(pyarmor_cmd)}")
result = subprocess.run(pyarmor_cmd, capture_output=True, text=True)
if result.stdout:
self.log(result.stdout)
if result.stderr:
self.log(result.stderr)
if result.returncode != 0:
self.log("PyArmor混淆失败,请检查上方错误信息!")
messagebox.showerror("PyArmor混淆失败", result.stderr or "未知错误")
return
# 这里要用原始文件名
py_file_to_pack = os.path.join(obf_dir, os.path.basename(main_py))
else:
py_file_to_pack = self.app_path.get()
cmd = [
'pyinstaller',
'--onefile',
'--noconsole',
'--clean',
f'--add-data={self.static_dir.get()}{os.pathsep}static',
f'--add-data={self.data_path.get()}{os.pathsep}.',
f'--distpath={os.path.abspath("dist")}',
f'--workpath={os.path.abspath("build")}'
]
# 添加 eventlet 及依赖 hidden-import
gevent_hidden_imports = [
'--hidden-import=gevent',
'--hidden-import=gevent.monkey',
'--hidden-import=geventwebsocket',
'--hidden-import=geventwebsocket.handler',
'--hidden-import=dns',
'--hidden-import=dns.rdtypes',
'--hidden-import=dns.rdtypes.ANY',
'--hidden-import=dns.rdtypes.IN',
'--hidden-import=dns.rdtypes.ANY.SOA',
'--hidden-import=dns.rdtypes.ANY.SRV',
'--hidden-import=dns.rdtypes.ANY.TXT',
'--hidden-import=dns.rdtypes.ANY.NS',
'--hidden-import=dns.rdtypes.ANY.CNAME',
'--hidden-import=dns.rdtypes.ANY.MX',
'--hidden-import=dns.rdtypes.ANY.PTR',
'--hidden-import=dns.rdtypes.ANY.AAAA',
'--hidden-import=dns.rdtypes.ANY.A',
'--hidden-import=dns.rdtypes.IN.A',
'--hidden-import=dns.rdtypes.IN.AAAA',
'--hidden-import=dns.rdtypes.IN.MX',
'--hidden-import=dns.rdtypes.IN.NS',
'--hidden-import=dns.rdtypes.IN.PTR',
'--hidden-import=dns.rdtypes.IN.SOA',
'--hidden-import=dns.rdtypes.IN.SRV',
'--hidden-import=dns.rdtypes.IN.TXT',
'--hidden-import=dns.asyncbackend',
'--hidden-import=dns.asyncquery',
'--hidden-import=dns.asyncresolver',
'--hidden-import=dns.e164',
'--hidden-import=dns.namedict',
'--hidden-import=dns.tsigkeyring',
'--hidden-import=dns.zone',
'--hidden-import=dns.dnssec',
'--hidden-import=dns.versioned',
'--hidden-import=greenlet',
]
cmd.extend(gevent_hidden_imports)
if self.icon_path.get():
cmd.append(f'--icon={self.icon_path.get()}')
cmd.append(py_file_to_pack)
self.log(f"执行命令: {' '.join(cmd)}")
with subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True
) as proc:
for line in iter(proc.stdout.readline, ''):
self.log(line.strip())
exit_code = proc.wait()
if exit_code == 0:
self.log("打包成功完成!")
messagebox.showinfo("成功", "EXE文件已生成在dist目录")
else:
self.log(f"打包失败,错误代码: {exit_code}")
messagebox.showerror("错误", f"打包失败,请检查日志\n错误代码: {exit_code}")
except Exception as e:
self.log(f"发生未预期的错误: {str(e)}")
messagebox.showerror("系统错误", str(e))
finally:
self.running = False
if os.path.exists("build"):
shutil.rmtree("build", ignore_errors=True)
if hasattr(self, "safe_pack") and self.safe_pack.get():
obf_dir = os.path.abspath(os.path.join(os.path.dirname(self.app_path.get()), "obf_temp"))
if os.path.exists(obf_dir):
shutil.rmtree(obf_dir, ignore_errors=True)
self.log("打包进程结束")
def log(self, message):
self.log_buffer.append(message)
self.update_log_display()
def clear_log(self):
self.log_text.config(state=tk.NORMAL)
self.log_text.delete(1.0, tk.END)
self.log_text.config(state=tk.DISABLED)
def update_log_display(self):
self.log_text.config(state=tk.NORMAL)
while self.log_buffer:
line = self.log_buffer.pop(0)
self.log_text.insert(tk.END, line + "\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
if __name__ == "__main__":
root = tk.Tk()
app = PackerApp(root)
root.protocol("WM_DELETE_WINDOW", lambda: root.destroy())
root.mainloop()