'''

(一)有前辈写了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()
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐