吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1478|回复: 46
上一主题 下一主题
收起左侧

[Python 原创] (3.19 22:22再次更新)python打包工具,可以添加指定的第三方库和数据目录

  [复制链接]
跳转到指定楼层
楼主
pnnhnjh 发表于 2025-3-16 19:22 回帖奖励
本帖最后由 pnnhnjh 于 2025-3-22 20:41 编辑

包含被打包主程序所在目录的全部文件,如果主程序所在目录存在add_libs.txt文件就自动打包里面的库,add_libs.txt文件内容为一行一个库,每一行的格式为“库名==版本号”,如“PyQt5==5.15.9”,点击开始打包会弹出输出文件夹对话框,打包完成自动打开输出文件夹。特别说明:1.打包时务必确保“upx.exe”文件与本脚本在同一目录下。2.用本脚本打包后的程序打包别的脚本可能有错误,推荐在开发环境使用!
20250319 22:22 再次更新了!使用upx压缩,添加简单使用说明和进度提示!

[Python] 纯文本查看 复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import os
import shutil
import subprocess
import threading
from tkinter import Tk, filedialog, messagebox, StringVar, Label, Button, Entry, Checkbutton, IntVar, Text
import sys
 
 
def center_window(window):
    """将窗口居中显示"""
    window.update_idletasks()
    width = window.winfo_width()
    height = window.winfo_height()
    x = (window.winfo_screenwidth() // 2) - (width // 2)
    y = (window.winfo_screenheight() // 2) - (height // 2)
    window.geometry(f'{width}x{height}+{x}+{y}')
    # 设置窗口置顶
    window.attributes('-topmost', True)
    # 延迟500毫秒取消窗口置顶
    window.after(500, lambda: window.attributes('-topmost', False))
 
 
def on_map(event):
    """窗口显示后调用此函数进行居中"""
    root.after(10, lambda: center_window(root))
 
 
def select_main_script():
    filepath = filedialog.askopenfilename(filetypes=[("Python files", "*.py")])
    if filepath:
        main_script.set(filepath)
        update_status_label("主脚本已选择,请继续选择其他文件或开始打包。")
 
 
def select_data_folder():
    folderpath = filedialog.askdirectory()
    if folderpath:
        data_folder.set(folderpath)
        update_status_label("数据文件夹已选择,请继续选择其他文件或开始打包。")
 
 
def select_icon_file():
    filepath = filedialog.askopenfilename(filetypes=[("Icon files", "*.ico")])
    if filepath:
        icon_path.set(filepath)
        update_status_label("图标文件已选择,请继续选择其他文件或开始打包。")
 
 
def open_folder(path):
    """仅适用于Windows平台的打开文件夹功能"""
    os.startfile(path)
 
 
def start_packaging_in_thread():
    threading.Thread(target=pack_application, daemon=True).start()
 
 
def generate_spec_file(main_script_path, datas, add_libs, output_dir, icon_path=None, hide_console=False):
    """生成.spec文件"""
    spec_filename = os.path.join(output_dir, os.path.splitext(os.path.basename(main_script_path))[0] + '.spec')
 
    main_script_utf8 = repr(main_script_path)[1:-1]
    datas_utf8 = [(repr(os.path.normpath(src).replace('\\', '/'))[1:-1], dest.replace('\\', '/')) for src, dest in datas]
 
    hidden_imports_utf8 = [repr(lib.split("==")[0].split(">=")[0].split("<=")[0].strip())[1:-1] for lib in add_libs]
 
    icon_str = f'icon="{icon_path}", ' if icon_path else ''
    console_str = 'console=False' if hide_console else 'console=True'
 
    with open(spec_filename, 'w', encoding='utf-8') as f:
        f.write(f"""
# -*- mode: python ; coding: utf-8 -*-
 
block_cipher = None
 
a = Analysis(
    ['{main_script_utf8}'],
    pathex=[],
    binaries=[],
    datas={datas_utf8},
    hiddenimports={hidden_imports_utf8},
    hookspath=[],
    runtime_hooks=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
 
exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name=os.path.splitext(os.path.basename('{main_script_utf8}'))[0],
    debug=False,
    strip=False,
    upx=True,
    {console_str},
    {icon_str}
    onefile=True
)
""")
    return spec_filename
 
 
def pack_application():
    status_label.config(state='normal')
    status_label.delete('1.0', 'end')
    insert_status("正在选择主脚本...")
    main_script_path = main_script.get()
    if not main_script_path:
        messagebox.showerror("错误", "请选择主脚本")
        return
 
    # 获取主脚本所在的文件夹和主脚本的名字
    main_script_dir = os.path.dirname(main_script_path)
    main_script_name = os.path.basename(main_script_path)
 
    insert_status("选择输出文件夹...")
    output_dir = filedialog.askdirectory(title="选择输出文件夹")
    if not output_dir:
        messagebox.showwarning("警告", "未选择输出文件夹,操作已取消。")
        return
 
    datas = []
    add_libs = []
 
    # 添加主脚本所在目录的所有文件(不包括子文件夹),同时剔除主脚本本身
    for item in os.listdir(main_script_dir):
        abs_path = os.path.join(main_script_dir, item)
        if os.path.isfile(abs_path) and item != main_script_name:  # 剔除主脚本本身
            # 将文件直接放入根目录
            datas.append((abs_path, '.'))
 
    # 添加数据文件夹内容到 datas(包括子目录)
    if data_folder.get():
        base_folder = data_folder.get()
        # 动态读取数据文件夹的名称
        target_folder_name = os.path.basename(base_folder)
        for root, _, files in os.walk(base_folder):
            for file in files:
                abs_path = os.path.join(root, file)
                rel_path = os.path.relpath(root, base_folder)
                # 目标路径为数据文件夹名称加上相对路径
                target_path = os.path.join(target_folder_name, rel_path) if rel_path != '.' else target_folder_name
                datas.append((abs_path, target_path))
 
    # 如果存在 add_libs.txt 文件,则读取其中的库列表
    add_lib_path = os.path.normpath(os.path.join(main_script_dir, 'add_libs.txt'))
    if os.path.exists(add_lib_path):
        with open(add_lib_path, encoding='utf-8') as f:
            add_libs = [line.strip() for line in f.readlines()]
 
    icon_path_value = icon_path.get()
    hide_console_value = hide_console.get()
 
    build_dir = os.path.join(main_script_dir, 'build_temp')
    os.makedirs(output_dir, exist_ok=True)
    os.makedirs(build_dir, exist_ok=True)
 
    insert_status("生成.spec文件...")
    spec_filename = generate_spec_file(main_script_path, datas, add_libs, output_dir, icon_path_value,
                                       hide_console_value)
 
    insert_status("执行PyInstaller命令,过程比较长,请耐心等待...")
 
    cmd = [
        'pyinstaller',
        '--distpath', output_dir,
        '--workpath', build_dir,
        '--noconfirm',
        '--clean',
        spec_filename
    ]
 
    startupinfo = subprocess.STARTUPINFO()
    startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
    startupinfo.wShowWindow = subprocess.SW_HIDE
 
    try:
        subprocess.run(cmd, check=True, startupinfo=startupinfo)
    except subprocess.CalledProcessError as e:
        messagebox.showerror("错误", f"PyInstaller 执行失败: {e}")
        return
 
    shutil.rmtree('__pycache__', ignore_errors=True)
    shutil.rmtree(build_dir, ignore_errors=True)
    try:
        os.remove(spec_filename)
    except OSError:
        pass
 
    open_folder(output_dir)
    insert_status("打包完成,等待新任务...")
    status_label.config(state='disabled')
 
 
def insert_status(message):
    status_label.insert('end', message + "\n")
    status_label.yview_moveto(1# 自动滚动到最新消息
 
 
def update_status_label(text):
    """更新状态标签的内容"""
    status_label.config(state='normal')
    status_label.delete('1.0', 'end')
    status_label.insert('end', text + "\n")
    status_label.yview_moveto(1# 自动滚动到最新消息
    status_label.config(state='disabled')
 
 
def create_gui():
    global root, main_script, data_folder, icon_path, hide_console, status_label
    root = Tk()
    root.title("Python 应用程序打包工具")
 
    # 调整窗口大小
    root.geometry('555x350')
 
    main_script = StringVar()
    data_folder = StringVar()
    icon_path = StringVar()
    hide_console = IntVar()
 
    initial_instructions = (
        '\u3000\u30001.打包主程序所在目录的全部文件,如果主程序所在目录存在add_libs.txt文件还会自动打包其指定的库,'
        '文件内容一行一个库,如“PyQt5==5.15.9”。'
        '\n\u3000\u30002.添加数据文件夹后需注意调用方法,如:if getattr(sys, "frozen", False):解包后主目录base_path=sys._MEIPASS,'
        'else:开发环境主目录base_path=os.path.dirname(os.path.abspath(__file__)),切换目录os.chdir(base_path),'
        '生成全路径文件名os.path.join(base_path,"数据文件夹名","文件名")。'
        '\n\u3000\u30003.打包时务必确保“upx.exe”文件与当前脚本在同一目录下!打包完成的可执行文件使用upx压缩,文件相对较小。'
    )
 
    Label(root, text="主脚本(必选):").grid(row=0, column=0, padx=5, pady=(20, 5), sticky='e')
    Entry(root, textvariable=main_script).grid(row=0, column=1, padx=5, pady=(20, 5), sticky='ew')
    Button(root, text="浏览", command=select_main_script).grid(row=0, column=2, padx=(5, 25), pady=(20, 5), sticky='ew')
 
    Label(root, text="图标文件(可选):").grid(row=1, column=0, padx=5, pady=5, sticky='e')
    Entry(root, textvariable=icon_path).grid(row=1, column=1, padx=5, pady=5, sticky='ew')
    Button(root, text="浏览", command=select_icon_file).grid(row=1, column=2, padx=(5, 25), pady=5, sticky='ew')
 
    Label(root, text="数据文件夹(可选):").grid(row=2, column=0, padx=5, pady=(5, 15), sticky='e')
    Entry(root, textvariable=data_folder).grid(row=2, column=1, padx=5, pady=(5, 15), sticky='ew')
    Button(root, text="浏览", command=select_data_folder).grid(row=2, column=2, padx=(5, 25), pady=(5, 15), sticky='ew')
 
    Checkbutton(root, text="隐藏控制台", variable=hide_console).grid(row=3, column=0, padx=(25, 5), pady=(5, 15), sticky='w')
    Button(root, text="开始打包", command=start_packaging_in_thread).grid(row=3, column=1, columnspan=1, padx=5,
                                                                      pady=(5, 15), sticky='ew')
 
    # 创建一个Text组件来显示状态信息,并允许文本自动换行
    status_label = Text(root, wrap='word', state='disabled', height=9, bg='white', fg='blue', relief='sunken', bd=1)
    status_label.grid(row=4, column=0, columnspan=3, padx=(25, 25), pady=(5, 15), sticky='nsew')
 
    # 设置初始状态说明
    update_status_label(initial_instructions)
 
    root.grid_columnconfigure(1, weight=1)
    root.bind("<Map>", on_map)
 
    root.mainloop()
 
 
if __name__ == "__main__":
    create_gui()





主界面.jpg (61.14 KB, 下载次数: 0)

主界面.jpg

upx.7z

466.03 KB, 下载次数: 35, 下载积分: 吾爱币 -1 CB

免费评分

参与人数 7吾爱币 +12 热心值 +6 收起 理由
NayaJosh + 1 谢谢@Thanks!
lsb2pojie + 1 + 1 谢谢@Thanks!
xy6538 + 1 谢谢@Thanks!
surepj + 1 + 1 用心讨论,共获提升!
qqhsx + 1 + 1 我很赞同!
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
dayu0 + 1 + 1 学习学习

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
matxi 发表于 2025-3-17 08:54
打包不仅涉及依赖库,还涉及到脚本需要的文件 我的建议是配置.spec 这样打包灵活一些 也更容易维护,还能自定义参数
推荐
mobei010 发表于 2025-3-22 21:58
import os
import shutil
import subprocess
import threading
import logging
from tkinter import Tk, filedialog, messagebox, StringVar, Label, Button, Entry, Checkbutton, IntVar, Text
import sys

# 配置日志记录
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')


def center_window(window):
    """将窗口居中显示"""
    window.update_idletasks()
    width = window.winfo_width()
    height = window.winfo_height()
    x = (window.winfo_screenwidth() // 2) - (width // 2)
    y = (window.winfo_screenheight() // 2) - (height // 2)
    window.geometry(f'{width}x{height}+{x}+{y}')
    # 设置窗口置顶
    window.attributes('-topmost', True)
    # 延迟500毫秒取消窗口置顶
    window.after(500, lambda: window.attributes('-topmost', False))


def on_map(event):
    """窗口显示后调用此函数进行居中"""
    root.after(10, lambda: center_window(root))


def select_main_script():
    filepath = filedialog.askopenfilename(filetypes=[("Python files", "*.py")])
    if filepath:
        main_script.set(filepath)
        update_status_label("主脚本已选择,请继续选择其他文件或开始打包。")
        logging.info(f"用户选择了主脚本: {filepath}")


def select_data_folder():
    folderpath = filedialog.askdirectory()
    if folderpath:
        data_folder.set(folderpath)
        update_status_label("数据文件夹已选择,请继续选择其他文件或开始打包。")
        logging.info(f"用户选择了数据文件夹: {folderpath}")


def select_icon_file():
    filepath = filedialog.askopenfilename(filetypes=[("Icon files", "*.ico")])
    if filepath:
        icon_path.set(filepath)
        update_status_label("图标文件已选择,请继续选择其他文件或开始打包。")
        logging.info(f"用户选择了图标文件: {filepath}")


def open_folder(path):
    """打开文件夹,支持不同操作系统"""
    if sys.platform.startswith('win'):
        os.startfile(path)
    elif sys.platform.startswith('darwin'):
        subprocess.run(['open', path])
    elif sys.platform.startswith('linux'):
        subprocess.run(['xdg-open', path])
    logging.info(f"尝试打开文件夹: {path}")


def start_packaging_in_thread():
    threading.Thread(target=pack_application, daemon=True).start()


def generate_spec_file(main_script_path, datas, add_libs, output_dir, icon_path=None, hide_console=False):
    """生成.spec文件"""
    spec_filename = os.path.join(output_dir, os.path.splitext(os.path.basename(main_script_path))[0] + '.spec')
    logging.info(f"开始生成 .spec 文件,目标路径: {spec_filename}")

    main_script_utf8 = repr(main_script_path)[1:-1]
    datas_utf8 = [(repr(os.path.normpath(src).replace('\\', '/'))[1:-1], dest.replace('\\', '/')) for src, dest in datas]

    hidden_imports_utf8 = [repr(lib.split("==")[0].split(">=")[0].split("<=")[0].strip())[1:-1] for lib in add_libs]

    icon_str = f'icon="{icon_path}", ' if icon_path else ''
    console_str = 'console=False' if hide_console else 'console=True'

    try:
        with open(spec_filename, 'w', encoding='utf-8') as f:
            f.write(f"""
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
    ['{main_script_utf8}'],
    pathex=[],
    binaries=[],
    datas={datas_utf8},
    hiddenimports={hidden_imports_utf8},
    hookspath=[],
    runtime_hooks=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name=os.path.splitext(os.path.basename('{main_script_utf8}'))[0],
    debug=False,
    strip=False,
    upx=True,
    {console_str},
    {icon_str}
    onefile=True
)
""")
        logging.info(f".spec 文件生成成功: {spec_filename}")
    except FileNotFoundError:
        logging.error(f"生成.spec文件时,找不到目标路径: {spec_filename}")
        messagebox.showerror("错误", f"生成.spec文件时,找不到目标路径: {spec_filename}")
        return None
    except PermissionError:
        logging.error(f"生成.spec文件时,没有写入权限: {spec_filename}")
        messagebox.showerror("错误", f"生成.spec文件时,没有写入权限: {spec_filename}")
        return None
    except Exception as e:
        logging.error(f"生成.spec文件时出现未知错误: {e},目标路径: {spec_filename}")
        messagebox.showerror("错误", f"生成.spec文件时出现未知错误: {e}")
        return None

    return spec_filename


def get_datas(main_script_dir, main_script_name, data_folder_path):
    """获取要打包的数据文件列表"""
    datas = []
    logging.info(f"开始获取主脚本所在目录({main_script_dir})的文件列表")
    # 添加主脚本所在目录的所有文件(不包括子文件夹),同时剔除主脚本本身
    try:
        for item in os.listdir(main_script_dir):
            abs_path = os.path.join(main_script_dir, item)
            if os.path.isfile(abs_path) and item != main_script_name:  # 剔除主脚本本身
                # 将文件直接放入根目录
                datas.append((abs_path, '.'))
        logging.info(f"成功获取主脚本所在目录({main_script_dir})的文件列表")
    except FileNotFoundError:
        logging.error(f"主脚本所在目录不存在: {main_script_dir}")
        messagebox.showerror("错误", f"主脚本所在目录不存在: {main_script_dir}")
        return []
    except PermissionError:
        logging.error(f"没有权限访问主脚本所在目录: {main_script_dir}")
        messagebox.showerror("错误", f"没有权限访问主脚本所在目录: {main_script_dir}")
        return []
    except Exception as e:
        logging.error(f"获取主脚本所在目录文件时出现未知错误: {e},目录路径: {main_script_dir}")
        messagebox.showerror("错误", f"获取主脚本所在目录文件时出现未知错误: {e}")
        return []

    # 添加数据文件夹内容到 datas(包括子目录)
    if data_folder_path:
        base_folder = data_folder_path
        # 动态读取数据文件夹的名称
        target_folder_name = os.path.basename(base_folder)
        logging.info(f"开始获取数据文件夹({base_folder})的内容")
        try:
            for root, _, files in os.walk(base_folder):
                for file in files:
                    abs_path = os.path.join(root, file)
                    rel_path = os.path.relpath(root, base_folder)
                    # 目标路径为数据文件夹名称加上相对路径
                    target_path = os.path.join(target_folder_name, rel_path) if rel_path != '.' else target_folder_name
                    datas.append((abs_path, target_path))
            logging.info(f"成功获取数据文件夹({base_folder})的内容")
        except FileNotFoundError:
            logging.error(f"数据文件夹不存在: {base_folder}")
            messagebox.showerror("错误", f"数据文件夹不存在: {base_folder}")
        except PermissionError:
            logging.error(f"没有权限访问数据文件夹: {base_folder}")
            messagebox.showerror("错误", f"没有权限访问数据文件夹: {base_folder}")
        except Exception as e:
            logging.error(f"获取数据文件夹内容时出现未知错误: {e},文件夹路径: {base_folder}")
            messagebox.showerror("错误", f"获取数据文件夹内容时出现未知错误: {e}")

    return datas


def get_add_libs(main_script_dir):
    """获取要添加的库列表"""
    add_libs = []
    # 如果存在 add_libs.txt 文件,则读取其中的库列表
    add_lib_path = os.path.normpath(os.path.join(main_script_dir, 'add_libs.txt'))
    if os.path.exists(add_lib_path):
        logging.info(f"开始读取 add_libs.txt 文件: {add_lib_path}")
        try:
            with open(add_lib_path, encoding='utf-8') as f:
                add_libs = [line.strip() for line in f.readlines()]
            logging.info(f"成功读取 add_libs.txt 文件: {add_lib_path}")
        except FileNotFoundError:
            logging.error(f"找不到 add_libs.txt 文件: {add_lib_path}")
            messagebox.showerror("错误", f"找不到 add_libs.txt 文件: {add_lib_path}")
        except PermissionError:
            logging.error(f"没有权限读取 add_libs.txt 文件: {add_lib_path}")
            messagebox.showerror("错误", f"没有权限读取 add_libs.txt 文件: {add_lib_path}")
        except Exception as e:
            logging.error(f"读取 add_libs.txt 文件时出现未知错误: {e},文件路径: {add_lib_path}")
            messagebox.showerror("错误", f"读取 add_libs.txt 文件时出现未知错误: {e}")
    return add_libs


def pack_application():
    status_label.config(state='normal')
    status_label.delete('1.0', 'end')
    insert_status("正在选择主脚本...")
    main_script_path = main_script.get()
    if not main_script_path:
        messagebox.showerror("错误", "请选择主脚本")
        return

    # 获取主脚本所在的文件夹和主脚本的名字
    main_script_dir = os.path.dirname(main_script_path)
    main_script_name = os.path.basename(main_script_path)

    insert_status("选择输出文件夹...")
    output_dir = filedialog.askdirectory(title="选择输出文件夹")
    if not output_dir:
        messagebox.showwarning("警告", "未选择输出文件夹,操作已取消。")
        return
    logging.info(f"用户选择的输出文件夹: {output_dir}")

    datas = get_datas(main_script_dir, main_script_name, data_folder.get())
    add_libs = get_add_libs(main_script_dir)

    icon_path_value = icon_path.get()
    hide_console_value = hide_console.get()

    build_dir = os.path.join(main_script_dir, 'build_temp')
    logging.info(f"开始创建目录: {output_dir} 和 {build_dir}")
    try:
        os.makedirs(output_dir, exist_ok=True)
        os.makedirs(build_dir, exist_ok=True)
        logging.info(f"成功创建目录: {output_dir} 和 {build_dir}")
    except FileNotFoundError:
        logging.error(f"创建目录时,找不到父目录: {output_dir} 或 {build_dir}")
        messagebox.showerror("错误", f"创建目录时,找不到父目录: {output_dir} 或 {build_dir}")
        return
    except PermissionError:
        logging.error(f"没有权限创建目录: {output_dir} 或 {build_dir}")
        messagebox.showerror("错误", f"没有权限创建目录: {output_dir} 或 {build_dir}")
        return
    except Exception as e:
        logging.error(f"创建目录时出现未知错误: {e},目录路径: {output_dir} 和 {build_dir}")
        messagebox.showerror("错误", f"创建目录时出现未知错误: {e}")
        return

    insert_status("生成.spec文件...")
    spec_filename = generate_spec_file(main_script_path, datas, add_libs, output_dir, icon_path_value,
                                       hide_console_value)
    if spec_filename is None:
        return

    insert_status("执行PyInstaller命令,过程比较长,请耐心等待...")
    logging.info(f"开始执行 PyInstaller 命令,使用 .spec 文件: {spec_filename}")

    cmd = [
        'pyinstaller',
        '--distpath', output_dir,
        '--workpath', build_dir,
        '--noconfirm',
        '--clean',
        spec_filename
    ]

    startupinfo = subprocess.STARTUPINFO()
    if sys.platform.startswith('win'):
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE

    try:
        process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
                                   startupinfo=startupinfo)
        while True:
            output = process.stdout.readline()
            if output == '' and process.poll() is not None:
                break
            if output:
                insert_status(output.strip())
        return_code = process.poll()
        if return_code != 0:
            raise subprocess.CalledProcessError(return_code, cmd)
        logging.info(f"PyInstaller 命令执行成功,使用 .spec 文件: {spec_filename}")
    except FileNotFoundError:
        logging.error("找不到 pyinstaller 命令,请确保已经安装。")
        messagebox.showerror("错误", "找不到 pyinstaller 命令,请确保已经安装。")
        return
    except subprocess.CalledProcessError as e:
        logging.error(f"PyInstaller 执行失败: {e},使用 .spec 文件: {spec_filename}")
        messagebox.showerror("错误", f"PyInstaller 执行失败: {e}")
        return
    except Exception as e:
        logging.error(f"执行过程中出现未知错误: {e},使用 .spec 文件: {spec_filename}")
        messagebox.showerror("错误", f"执行过程中出现未知错误: {e}")
        return

    # 删除 __pycache__ 目录
    pycache_dir = os.path.join(main_script_dir, '__pycache__')
    if os.path.exists(pycache_dir):
        logging.info(f"开始删除 __pycache__ 目录: {pycache_dir}")
        try:
            shutil.rmtree(pycache_dir, ignore_errors=True)
            logging.info(f"成功删除 __pycache__ 目录: {pycache_dir}")
        except PermissionError:
            logging.error(f"没有权限删除 __pycache__ 目录: {pycache_dir}")
            messagebox.showerror("错误", f"没有权限删除 __pycache__ 目录: {pycache_dir}")
        except Exception as e:
            logging.error(f"删除 __pycache__ 目录时出现未知错误: {e},目录路径: {pycache_dir}")
            messagebox.showerror("错误", f"删除 __pycache__ 目录时出现未知错误: {e}")

    logging.info(f"开始删除 build 目录: {build_dir}")
    try:
        shutil.rmtree(build_dir, ignore_errors=True)
        logging.info(f"成功删除 build 目录: {build_dir}")
    except PermissionError:
        logging.error(f"没有权限删除 build 目录: {build_dir}")
        messagebox.showerror("错误", f"没有权限删除 build 目录: {build_dir}")
    except Exception as e:
        logging.error(f"删除 build 目录时出现未知错误: {e},目录路径: {build_dir}")
        messagebox.showerror("错误", f"删除 build 目录时出现未知错误: {e}")

    logging.info(f"开始删除 .spec 文件: {spec_filename}")
    try:
        os.remove(spec_filename)
        logging.info(f"成功删除 .spec 文件: {spec_filename}")
    except FileNotFoundError:
        logging.error(f"找不到 .spec 文件: {spec_filename}")
    except PermissionError:
        logging.error(f"没有权限删除 .spec 文件: {spec_filename}")
        messagebox.showerror("错误", f"没有权限删除 .spec 文件: {spec_filename}")
    except Exception as e:
        logging.error(f"删除 .spec 文件时出现未知错误: {e},文件路径: {spec_filename}")
        messagebox.showerror("错误", f"删除 .spec 文件时出现未知错误: {e}")

    open_folder(output_dir)
    insert_status("打包完成,等待新任务...")
    status_label.config(state='disabled')


def insert_status(message):
    status_label.insert('end', message + "\n")
    status_label.yview_moveto(1)  # 自动滚动到最新消息
    logging.info(message)


def update_status_label(text):
    """更新状态标签的内容"""
    status_label.config(state='normal')
    status_label.delete('1.0', 'end')
    status_label.insert('end', text + "\n")
    status_label.yview_moveto(1)  # 自动滚动到最新消息
    status_label.config(state='disabled')


def create_gui():
    global root, main_script, data_folder, icon_path, hide_console, status_label
    root = Tk()
    root.title("Python 应用程序打包工具")

    # 调整窗口大小
    root.geometry('555x350')

    main_script = StringVar()
    data_folder = StringVar()
    icon_path = StringVar()
    hide_console = IntVar()

    initial_instructions = (
        '\u3000\u30001.打包主程序所在目录的全部文件,如果主程序所在目录存在add_libs.txt文件还会自动打包其指定的库,'
        '文件内容一行一个库,如“PyQt5==5.15.9”。'
        '\n\u3000\u30002.添加数据文件夹后需注意调用方法,如:if getattr(sys, "frozen", False):解包后主目录base_path=sys._MEIPASS,'
        'else:开发环境主目录base_path=os.path.dirname(os.path.abspath(__file__)),切换目录os.chdir(base_path),'
        '生成全路径文件名os.path.join(base_path,"数据文件夹名","文件名")。'
        '\n\u3000\u30003.打包时务必确保“upx.exe”文件与当前脚本在同一目录下!打包完成的可执行文件使用upx压缩,文件相对较小。'
    )

    Label(root, text="主脚本(必选):").grid(row=0, column=0, padx=5, pady=(20, 5), sticky='e')
    Entry(root, textvariable=main_script).grid(row=0, column=1, padx=5, pady=(20, 5), sticky='ew')
    Button(root, text="浏览", command=select_main_script).grid(row=0, column=2, padx=(5, 25), pady=(20, 5), sticky='ew')

    Label(root, text="图标文件(可选):").grid(row=1, column=0, padx=5, pady=5, sticky='e')
    Entry(root, textvariable=icon_path).grid(row=1, column=1, padx=5, pady=5, sticky='ew')
    Button(root, text="浏览", command=select_icon_file).grid(row=1, column=2, padx=(5, 25), pady=5, sticky='ew')

    Label(root, text="数据文件夹(可选):").grid(row=2, column=0, padx=5, pady=(5, 15), sticky='e')
    Entry(root, textvariable=data_folder).grid(row=2, column=1, padx=5, pady=(5, 15), sticky='ew')
    Button(root, text="浏览", command=select_data_folder).grid(row=2, column=2, padx=(5, 25), pady=(5, 15), sticky='ew')

    Checkbutton(root, text="隐藏控制台", variable=hide_console).grid(row=3, column=0, padx=(25, 5), pady=(5, 15), sticky='w')
    Button(root, text="开始打包", command=start_packaging_in_thread).grid(row=3, column=1, columnspan=1, padx=5,
                                                                          pady=(5, 15), sticky='ew')

    # 创建一个Text组件来显示状态信息,并允许文本自动换行
    status_label = Text(root, wrap='word', state='disabled', height=9, bg='white', fg='blue', relief='sunken', bd=1)
    status_label.grid(row=4, column=0, columnspan=3, padx=(25, 25), pady=(5, 15), sticky='nsew')

    # 设置初始状态说明
    update_status_label(initial_instructions)

    root.grid_columnconfigure(1, weight=1)
    root.bind("<Map>", on_map)

    root.mainloop()


if __name__ == "__main__":
    create_gui()
   
沙发
wissle 发表于 2025-3-16 20:30
3#
realcsnake 发表于 2025-3-16 20:44
谢谢无私分享
4#
wuaiwuxiwuxuexi 发表于 2025-3-16 20:59
感谢分享收藏,后续用得到
5#
wushang01 发表于 2025-3-16 21:05
感谢分享,正好需要
6#
qjscn 发表于 2025-3-16 21:28
谢谢分享,正需要
7#
wang5588 发表于 2025-3-16 21:52
正好用午得着
8#
jtjt68 发表于 2025-3-16 22:07
没抢到板凳
9#
wang5588 发表于 2025-3-16 22:14
打开后提示:Failed to start embedded python interpreter!     启动嵌入式python解释器失败!
10#
inhk 发表于 2025-3-16 22:32
谢谢楼主的分享!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-4-1 19:12

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表