统信系统小程序(四)linux环境下的python程序打包Nuitka工具
·
'''
(一)有前辈写了win环境的Nuitka打包工具,觉得很好用。打包的效果很小,大小只有几百K。linux环境下测试Nuitka没有那么惊艳,打包时间很长,估计1小时。打包效果也不好,一半都要几十M。不知道问题出在哪,但是暂时能用,当然里边也包括了pyinstaller功能。运行需要什么库就装什么。例如pip install nuitka。
(二)Nuitka打包不支持tkinterdnd2拖拽,Linux下会找不到dnd目录,因此弹出tk灰色小窗口。因此应使用pyinstaller打包,文件大小会翻倍。
加速编译:sudo apt-get install ccache#安装 ccache(强烈推荐)
sudo apt-get install gcc-10 g++-10#升级 GCC 编译器(可选)
(三)程序(或打包时引入的 Python 库)有时无视现有会话总线,强制调用linux已废弃的 dbus-launch 命令。现代 Linux 桌面(含 UOS)已全面采用 systemd 管理 D-Bus,UOS 20+ 默认使用 dbus-broker,不再预装 dbus-launch,因此直接报 EOF。是否在打包命令中添加:--exclude-module dbus --exclude-module dbus.mainloop(若实际未用到)以下是代码:
'''
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import subprocess
import os
import sys
import threading
def get_resource_path(relative_path):
"""
获取资源文件的绝对路径,兼容打包前后的情况
"""
try:
# PyInstaller创建的临时文件夹
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
class PyToLinuxEXE:
def __init__(self):
self.root = tk.Tk()
self.root.title("Python程序打包工具")
self.root.geometry("800x600")
# 设置背景色为淡黄色
self.bg_color = "#FFFACD" # 淡黄色
self.root.configure(bg=self.bg_color)
self.source_file = tk.StringVar()
self.output_dir = tk.StringVar()
self.output_name = tk.StringVar()
self.one_file = tk.BooleanVar(value=True)
self.console = tk.BooleanVar(value=False)
self.icon_file = tk.StringVar()
self.packaging_tool = tk.StringVar(value="pyinstaller") # 默认打包工具
# 设置默认字体
self.default_font = ("fangsong ti", 12)
# 设置全局字体
self.set_global_font()
# 设置糖果色
self.candy_colors = {
"button_bg": "#FFB6C1", # 粉红色
"button_fg": "#8B008B", # 深洋红色
"button_active_bg": "#FF69B4", # 热粉红色
"browse_button_bg": "#98FB98", # 淡绿色
"browse_button_fg": "#006400", # 深绿色
"browse_button_active_bg": "#00FF7F", # 春绿色
"action_button_bg": "#87CEFA", # 淡蓝色
"action_button_fg": "#000080", # 海军蓝
"action_button_active_bg": "#4169E1" # 皇家蓝
}
# 设置输出默认值
self.output_dir.set(os.path.expanduser("~/test/xuanzhuan"))
# 设置源文件默认值(如果文件存在)
default_source = "/home/huanghe/PycharmProjects/pythonEMY"
if os.path.exists(default_source):
self.source_file.set(default_source)
# 自动设置输出文件名
self.output_name.set(os.path.splitext(os.path.basename(default_source))[0])
# 设置图标文件默认值(如果文件存在)
default_icon = "/home/huanghe/test/file.ico"
if os.path.exists(default_icon):
self.icon_file.set(default_icon)
self.create_widgets()
def set_global_font(self):
# 设置默认字体
self.default_font = ("fangsong ti", 12)
# 配置根窗口的默认字体
self.root.option_add("*Font", self.default_font)
# 创建自定义样式
style = ttk.Style()
style.configure(".", font=self.default_font, background=self.bg_color)
style.configure("TButton", font=self.default_font)
style.configure("TLabel", font=self.default_font, background=self.bg_color)
style.configure("TCheckbutton", font=self.default_font, background=self.bg_color)
style.configure("TEntry", font=self.default_font)
style.configure("TFrame", background=self.bg_color)
def create_widgets(self):
main_frame = tk.Frame(self.root, bg=self.bg_color)
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 控制区域
control_frame = tk.Frame(main_frame, bg=self.bg_color)
control_frame.pack(fill=tk.X, pady=3)
# 源文件选择
input_frame = tk.Frame(control_frame, bg=self.bg_color)
input_frame.pack(fill=tk.X, pady=2)
tk.Label(input_frame, text="源文件:", width=10, anchor=tk.W, font=self.default_font, bg=self.bg_color).pack(
side=tk.LEFT)
tk.Entry(input_frame, textvariable=self.source_file, font=self.default_font, bg="white").pack(side=tk.LEFT,
fill=tk.X,
expand=True,
padx=3)
tk.Button(input_frame, text="浏览", command=self.browse_source, width=8,
bg=self.candy_colors["browse_button_bg"], fg=self.candy_colors["browse_button_fg"],
activebackground=self.candy_colors["browse_button_active_bg"], font=self.default_font,
relief=tk.RAISED, bd=2).pack(side=tk.LEFT, padx=3)
# 输出目录选择
output_frame = tk.Frame(control_frame, bg=self.bg_color)
output_frame.pack(fill=tk.X, pady=2)
tk.Label(output_frame, text="输出目录:", width=10, anchor=tk.W, font=self.default_font, bg=self.bg_color).pack(
side=tk.LEFT)
tk.Entry(output_frame, textvariable=self.output_dir, font=self.default_font, bg="white").pack(side=tk.LEFT,
fill=tk.X,
expand=True,
padx=3)
tk.Button(output_frame, text="浏览", command=self.browse_output, width=8,
bg=self.candy_colors["browse_button_bg"], fg=self.candy_colors["browse_button_fg"],
activebackground=self.candy_colors["browse_button_active_bg"], font=self.default_font,
relief=tk.RAISED, bd=2).pack(side=tk.LEFT, padx=3)
# 输出文件名
name_frame = tk.Frame(control_frame, bg=self.bg_color)
name_frame.pack(fill=tk.X, pady=2)
tk.Label(name_frame, text="输出名称:", width=10, anchor=tk.W, font=self.default_font, bg=self.bg_color).pack(
side=tk.LEFT)
tk.Entry(name_frame, textvariable=self.output_name, font=self.default_font, bg="white").pack(side=tk.LEFT,
fill=tk.X,
expand=True,
padx=3)
# 打包工具选择
tool_frame = tk.Frame(control_frame, bg=self.bg_color)
tool_frame.pack(fill=tk.X, pady=2)
tk.Label(tool_frame, text="打包工具:", width=10, anchor=tk.W, font=self.default_font, bg=self.bg_color).pack(
side=tk.LEFT)
tool_combo = ttk.Combobox(tool_frame, textvariable=self.packaging_tool,
values=["pyinstaller", "cx_freeze", "nuitka", "briefcase"],
state="readonly", font=self.default_font, width=15)
tool_combo.pack(side=tk.LEFT, padx=3)
tool_combo.set("nuitka") # 设置默认值
# 选项区域
options_frame = tk.Frame(control_frame, bg=self.bg_color)
options_frame.pack(fill=tk.X, pady=10)
tk.Label(options_frame, text="打包选项:", width=10, anchor=tk.W, font=self.default_font, bg=self.bg_color).pack(
side=tk.LEFT)
options_container = tk.Frame(options_frame, bg=self.bg_color)
options_container.pack(side=tk.LEFT, fill=tk.X, expand=True)
tk.Checkbutton(options_container, text="生成单个文件", variable=self.one_file,
font=self.default_font, bg=self.bg_color, selectcolor="white").pack(
side=tk.LEFT, padx=10)
tk.Checkbutton(options_container, text="无控制台窗口", variable=self.console,
font=self.default_font, bg=self.bg_color, selectcolor="white").pack(
side=tk.LEFT, padx=10)
# 图标文件选择
icon_frame = tk.Frame(control_frame, bg=self.bg_color)
icon_frame.pack(fill=tk.X, pady=2)
tk.Label(icon_frame, text="图标文件:", width=10, anchor=tk.W, font=self.default_font, bg=self.bg_color).pack(
side=tk.LEFT)
tk.Entry(icon_frame, textvariable=self.icon_file, font=self.default_font, bg="white").pack(side=tk.LEFT,
fill=tk.X,
expand=True, padx=3)
tk.Button(icon_frame, text="浏览", command=self.browse_icon, width=8,
bg=self.candy_colors["browse_button_bg"], fg=self.candy_colors["browse_button_fg"],
activebackground=self.candy_colors["browse_button_active_bg"], font=self.default_font,
relief=tk.RAISED, bd=2).pack(side=tk.LEFT, padx=3)
# 按钮区域
button_frame = tk.Frame(control_frame, bg=self.bg_color)
button_frame.pack(fill=tk.X, pady=10)
self.pack_button = tk.Button(button_frame, text="开始打包", command=self.start_packaging,
bg=self.candy_colors["action_button_bg"], fg=self.candy_colors["action_button_fg"],
activebackground=self.candy_colors["action_button_active_bg"],
font=self.default_font, relief=tk.RAISED, bd=2)
self.pack_button.pack(side=tk.RIGHT, padx=5)
tk.Button(button_frame, text="退出", command=self.root.quit,
bg=self.candy_colors["button_bg"], fg=self.candy_colors["button_fg"],
activebackground=self.candy_colors["button_active_bg"],
font=self.default_font, relief=tk.RAISED, bd=2).pack(side=tk.RIGHT, padx=5)
# 内容区域
content_frame = tk.Frame(main_frame, bg=self.bg_color)
content_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# 日志区域
log_container = tk.Frame(content_frame, bg=self.bg_color)
log_container.pack(fill=tk.BOTH, expand=True, padx=2)
tk.Label(log_container, text="打包日志", font=self.default_font, bg=self.bg_color).pack(anchor=tk.W)
log_frame = tk.Frame(log_container, bg=self.bg_color)
log_frame.pack(fill=tk.BOTH, expand=True)
self.log_text = tk.Text(log_frame, height=20, font=self.default_font, bg="white")
scrollbar = tk.Scrollbar(log_frame, command=self.log_text.yview)
self.log_text.configure(yscrollcommand=scrollbar.set)
self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 进度条
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
self.progress.pack(fill=tk.X, padx=5, pady=5)
def browse_source(self):
filename = filedialog.askopenfilename(
title="选择Python文件",
filetypes=[("Python files", "*.py"), ("All files", "*.*")]
)
if filename:
self.source_file.set(filename)
# 自动设置输出文件名
if not self.output_name.get():
self.output_name.set(os.path.splitext(os.path.basename(filename))[0])
def browse_output(self):
directory = filedialog.askdirectory(
title="选择输出目录"
)
if directory:
self.output_dir.set(directory)
def browse_icon(self):
filename = filedialog.askopenfilename(
title="选择图标文件",
filetypes=[("Icon files", "*.ico *.png"), ("All files", "*.*")]
)
if filename:
self.icon_file.set(filename)
def log_message(self, message):
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.root.update_idletasks()
def check_anaconda_pathlib(self):
"""检查并提示 Anaconda 环境中的 pathlib 冲突问题"""
try:
import pathlib
pathlib_path = pathlib.__file__
# 检测是否在 Anaconda 环境中
if 'anaconda' in pathlib_path.lower() or 'site-packages' in pathlib_path:
# 检查是否是后向移植版本
if hasattr(pathlib, '__version__'):
self.log_message("=" * 60)
self.log_message("⚠ 严重: 检测到 Anaconda 环境中的 pathlib 后向移植包")
self.log_message("=" * 60)
self.log_message("这会导致 PyInstaller 打包失败!")
self.log_message("")
self.log_message("请在终端执行以下命令移除冲突包:")
self.log_message(" conda remove pathlib")
self.log_message(" 或")
self.log_message(" pip uninstall pathlib pathlib2")
self.log_message("")
self.log_message("执行完成后,重新启动本打包工具")
self.log_message("=" * 60)
return False
except Exception as e:
self.log_message(f"pathlib 检查异常: {e}")
return True
def start_packaging(self):
# 在后台线程中执行打包操作
thread = threading.Thread(target=self.package_executable)
thread.daemon = True
thread.start()
def package_executable(self):
# 禁用打包按钮并开始进度条
self.pack_button.config(state=tk.DISABLED)
self.progress.start()
try:
# 检查 Anaconda pathlib 冲突
has_pathlib_conflict = not self.check_anaconda_pathlib()
# 如果检测到 pathlib 冲突,直接终止
if has_pathlib_conflict:
messagebox.showerror("环境错误",
"检测到 Anaconda pathlib 后向移植包冲突!\n\n"
"请先在终端执行:\n"
" conda remove pathlib\n\n"
"然后重新启动打包工具")
return
# 检查输入
source_file = self.source_file.get()
if not source_file:
messagebox.showwarning("警告", "请选择源文件")
return
if not os.path.exists(source_file):
messagebox.showwarning("警告", "源文件不存在")
return
output_dir = self.output_dir.get()
if not output_dir:
messagebox.showwarning("警告", "请选择输出目录")
return
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_name = self.output_name.get()
if not output_name:
output_name = os.path.splitext(os.path.basename(source_file))[0]
# 获取选择的打包工具
tool = self.packaging_tool.get()
# 根据选择的工具构建命令
if tool == "pyinstaller":
cmd = self.build_pyinstaller_command(source_file, output_dir, output_name)
elif tool == "cx_freeze":
cmd = self.build_cx_freeze_command(source_file, output_dir, output_name)
elif tool == "nuitka":
cmd = self.build_nuitka_command(source_file, output_dir, output_name)
elif tool == "briefcase":
cmd = self.build_briefcase_command(source_file, output_dir, output_name)
else:
messagebox.showerror("错误", f"不支持的打包工具: {tool}")
return
self.log_message(f"使用工具: {tool}")
self.log_message("执行命令: " + " ".join(cmd))
self.log_message("开始打包...")
# 执行命令
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
cwd=os.path.dirname(source_file) if os.path.isfile(source_file) else os.getcwd()
)
# 实时显示输出
for line in process.stdout:
self.log_message(line.strip())
process.wait()
if process.returncode == 0:
self.log_message("打包完成!")
messagebox.showinfo("成功", "可执行文件已生成")
else:
self.log_message("打包失败!")
messagebox.showerror("错误", "打包过程中出现错误,请查看日志")
except Exception as e:
self.log_message(f"发生错误: {str(e)}")
messagebox.showerror("错误", f"发生错误: {str(e)}")
finally:
# 重新启用打包按钮并停止进度条
self.progress.stop()
self.pack_button.config(state=tk.NORMAL)
def build_pyinstaller_command(self, source_file, output_dir, output_name):
"""构建PyInstaller命令"""
cmd = [sys.executable, "-m", "PyInstaller"]
# 添加选项
if self.one_file.get():
cmd.append("--onefile")
if self.console.get():
cmd.append("--noconsole")
else:
cmd.append("--console")
if self.icon_file.get():
icon_file = self.icon_file.get()
if os.path.exists(icon_file):
cmd.extend(["--icon", icon_file])
# 设置输出目录
cmd.extend(["--distpath", output_dir])
cmd.extend(["--workpath", os.path.join(output_dir, "build")])
cmd.extend(["--specpath", output_dir])
# 启用必要的隐藏导入
cmd.append("--hidden-import=PIL")
cmd.append("--hidden-import=PIL.Image")
cmd.append("--hidden-import=tkinter")
# 处理 tkinterdnd2 支持
try:
import tkinterdnd2
tkdnd_package_path = os.path.dirname(tkinterdnd2.__file__)
# 包含整个 tkinterdnd2 包
cmd.append("--collect-all=tkinterdnd2")
# 检查并记录 tkdnd 子目录
tkdnd_lib_path = os.path.join(tkdnd_package_path, "tkdnd")
if os.path.exists(tkdnd_lib_path):
files = os.listdir(tkdnd_lib_path)
so_files = [f for f in files if f.endswith('.so')]
tcl_files = [f for f in files if f.endswith('.tcl')]
self.log_message(f"tkdnd 库文件 ({len(so_files)} 个 .so): {so_files}")
self.log_message(f"tkdnd TCL 文件 ({len(tcl_files)} 个 .tcl): {tcl_files}")
if so_files or tcl_files:
# Linux 使用 : 作为分隔符,Windows 使用 ;
separator = ":" if sys.platform != "win32" else ";"
# 包含整个 tkdnd 目录
cmd.append(f"--add-data={tkdnd_lib_path}{os.sep}*{separator}tkinterdnd2/tkdnd")
self.log_message(f"已添加 tkdnd 数据文件: {tkdnd_lib_path}")
else:
self.log_message("警告:tkdnd 目录中没有 .so 或 .tcl 文件!")
self.log_message("建议执行: pip uninstall tkinterdnd2 && pip install tkinterdnd2")
else:
self.log_message(f"警告:未找到 tkdnd 子目录: {tkdnd_lib_path}")
except ImportError:
self.log_message("警告:未检测到 tkinterdnd2,拖拽功能将不可用")
# 添加源文件
cmd.append(source_file)
return cmd
def build_cx_freeze_command(self, source_file, output_dir, output_name):
"""构建cx_Freeze命令"""
cmd = [sys.executable, "-m", "cx_Freeze", source_file]
# 添加选项
cmd.extend(["--target-dir", output_dir])
if self.icon_file.get():
icon_file = self.icon_file.get()
if os.path.exists(icon_file):
cmd.extend(["--icon", icon_file])
return cmd
def build_nuitka_command(self, source_file, output_dir, output_name):
"""构建Nuitka命令 - 针对取证单程序优化版"""
cmd = [sys.executable, "-m", "nuitka", "--standalone"]
# 启用必要的插件
cmd.append("--enable-plugin=tk-inter")
# 添加选项
cmd.extend(["--output-dir=" + output_dir])
if self.one_file.get():
cmd.append("--onefile")
# 解决Anaconda环境问题
cmd.append("--static-libpython=no")
# 优化和清理选项
cmd.append("--remove-output")
cmd.append("--jobs=4")
cmd.append("--clean-cache=all")
# 处理matplotlib警告
cmd.append("--enable-plugin=no-qt")
# 排除D-Bus相关模块(UOS系统不需要,避免dbus-launch错误)
cmd.append("--nofollow-import-to=dbus")
cmd.append("--nofollow-import-to=dbus.mainloop")
cmd.append("--nofollow-import-to=gi.repository.Gio")
cmd.append("--nofollow-import-to=gi.repository.GLib")
# 关键修复:显式包含 tkinterdnd2 模块及其数据文件
try:
import tkinterdnd2
tkdnd_package_path = os.path.dirname(tkinterdnd2.__file__)
# 包含整个 tkinterdnd2 包
cmd.append("--include-package=tkinterdnd2")
# 检查并记录 tkdnd 子目录
tkdnd_lib_path = os.path.join(tkdnd_package_path, "tkdnd")
if os.path.exists(tkdnd_lib_path):
files = os.listdir(tkdnd_lib_path)
so_files = [f for f in files if f.endswith('.so')]
tcl_files = [f for f in files if f.endswith('.tcl')]
self.log_message(f"tkdnd 库文件 ({len(so_files)} 个 .so): {so_files}")
self.log_message(f"tkdnd TCL 文件 ({len(tcl_files)} 个 .tcl): {tcl_files}")
if so_files or tcl_files:
# 以数据文件形式包含 tkdnd 目录
cmd.append(f"--include-data-dir={tkdnd_lib_path}=tkinterdnd2/tkdnd")
self.log_message(f"已添加 tkdnd 数据目录: {tkdnd_lib_path}")
else:
self.log_message("警告:tkdnd 目录中没有 .so 或 .tcl 文件!")
self.log_message("建议执行: pip uninstall tkinterdnd2 && pip install tkinterdnd2")
else:
self.log_message(f"警告:未找到 tkdnd 子目录: {tkdnd_lib_path}")
except ImportError:
self.log_message("警告:未检测到 tkinterdnd2,拖拽功能将不可用")
# 如果你的程序需要特定的DLL或数据文件,可以在这里添加
# 例如,如果需要包含模板文件:
# cmd.append("--include-data-file=template.xlsx=template.xlsx")
# Linux图标处理 - 修复图标参数
if self.icon_file.get():
icon_file = self.icon_file.get()
if os.path.exists(icon_file):
# 对于ICO文件
if icon_file.lower().endswith('.ico'):
cmd.extend(["--windows-icon-from-ico=" + icon_file])
elif icon_file.lower().endswith(('.png', '.jpg', '.jpeg')):
# 对于PNG/JPG文件
cmd.extend(["--linux-onefile-icon=" + icon_file])
# 添加源文件
cmd.append(source_file)
return cmd
def build_briefcase_command(self, source_file, output_dir, output_name):
"""构建Briefcase命令"""
# Briefcase通常需要更复杂的项目结构,这里提供一个基础命令
cmd = [sys.executable, "-m", "briefcase", "create"]
# 设置输出目录
# 注意:Briefcase的参数可能需要根据具体项目结构调整
return cmd
def run(self):
self.root.mainloop()
if __name__ == "__main__":
app = PyToLinuxEXE()
app.run()
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)