pyinstaller-tool 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 siwpan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ prune .venv
2
+ prune dist
3
+ prune build
4
+ prune release
5
+ prune __pycache__
6
+ prune logs
7
+ prune afo
8
+ prune dist_pro
9
+ global-exclude *.pyc
10
+ global-exclude *.exe
11
+ global-exclude *.ico
12
+ global-exclude *.bat
13
+ global-exclude *.spec
14
+ global-exclude *.wav
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyinstaller-tool
3
+ Version: 1.0.0
4
+ Summary: PyInstaller 批量打包工具集(基础/专业/全自动 GUI + 批处理)
5
+ Author: siwpan
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitee.com/siwpan/pyinstaller
8
+ Project-URL: Repository, https://gitee.com/siwpan/pyinstaller
9
+ Project-URL: Changelog, https://gitee.com/siwpan/pyinstaller/blob/master/CHANGELOG.md
10
+ Keywords: pyinstaller,packaging,exe,gui,build
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pyinstaller
15
+ Requires-Dist: Pillow
16
+ Dynamic: license-file
17
+
18
+ # PyInstaller 批量打包工具集
19
+
20
+ > 仓库:<https://gitee.com/siwpan/pyinstaller> | 许可:MIT(见 [LICENSE](LICENSE))
21
+
22
+ 多功能 PyInstaller 批量打包工具箱,从基础到专业级打包。
23
+
24
+ ## 功能
25
+
26
+ - **基础版 (all.py)** — Tkinter GUI,勾选 .py 文件批量打包
27
+ - **专业版 (packer_pro.py)** — 强化 GUI,支持单文件/多文件架构、PNG→ICO、数字签名 (signtool)、附加数据、隐藏导入
28
+ - **全自动 (全宝.bat)** — 自动打包当前目录所有 .py 文件
29
+ - **智能版 (全能宝.bat)** — 自动检测 tkinter/PyQt/PySide 选择 GUI 模式,支持 UPX 压缩
30
+ - **通用打包.bat** — 手动选择单个 .py 打包
31
+ - **图标.bat** — 图标格式转换工具
32
+
33
+ ## 快速开始
34
+
35
+ ```bash
36
+ # 安装依赖
37
+ pip install pyinstaller Pillow
38
+
39
+ # 基础 GUI
40
+ python all.py
41
+
42
+ # 专业 GUI
43
+ python packer_pro.py
44
+
45
+ # 批处理打包
46
+ 全宝.bat
47
+ ```
48
+
49
+ ## 项目结构
50
+
51
+ ```
52
+ pyinstaller/
53
+ ├── all.py # 基础版 GUI
54
+ ├── packer_pro.py # 专业版 GUI (增强功能)
55
+ ├── 全宝.bat # 全自动打包批处理
56
+ ├── 全能宝.bat # 智能打包批处理
57
+ ├── 通用打包.bat # 手动单文件打包
58
+ ├── 图标.bat # 图标转换
59
+ ├── 图标.ico # 默认图标
60
+ └── dist/ # 打包输出
61
+ ```
62
+
63
+ ## 依赖
64
+
65
+ - Python >= 3.8
66
+ - pyinstaller, Pillow (可选), Windows SDK signtool (可选), UPX (可选)
@@ -0,0 +1,49 @@
1
+ # PyInstaller 批量打包工具集
2
+
3
+ > 仓库:<https://gitee.com/siwpan/pyinstaller> | 许可:MIT(见 [LICENSE](LICENSE))
4
+
5
+ 多功能 PyInstaller 批量打包工具箱,从基础到专业级打包。
6
+
7
+ ## 功能
8
+
9
+ - **基础版 (all.py)** — Tkinter GUI,勾选 .py 文件批量打包
10
+ - **专业版 (packer_pro.py)** — 强化 GUI,支持单文件/多文件架构、PNG→ICO、数字签名 (signtool)、附加数据、隐藏导入
11
+ - **全自动 (全宝.bat)** — 自动打包当前目录所有 .py 文件
12
+ - **智能版 (全能宝.bat)** — 自动检测 tkinter/PyQt/PySide 选择 GUI 模式,支持 UPX 压缩
13
+ - **通用打包.bat** — 手动选择单个 .py 打包
14
+ - **图标.bat** — 图标格式转换工具
15
+
16
+ ## 快速开始
17
+
18
+ ```bash
19
+ # 安装依赖
20
+ pip install pyinstaller Pillow
21
+
22
+ # 基础 GUI
23
+ python all.py
24
+
25
+ # 专业 GUI
26
+ python packer_pro.py
27
+
28
+ # 批处理打包
29
+ 全宝.bat
30
+ ```
31
+
32
+ ## 项目结构
33
+
34
+ ```
35
+ pyinstaller/
36
+ ├── all.py # 基础版 GUI
37
+ ├── packer_pro.py # 专业版 GUI (增强功能)
38
+ ├── 全宝.bat # 全自动打包批处理
39
+ ├── 全能宝.bat # 智能打包批处理
40
+ ├── 通用打包.bat # 手动单文件打包
41
+ ├── 图标.bat # 图标转换
42
+ ├── 图标.ico # 默认图标
43
+ └── dist/ # 打包输出
44
+ ```
45
+
46
+ ## 依赖
47
+
48
+ - Python >= 3.8
49
+ - pyinstaller, Pillow (可选), Windows SDK signtool (可选), UPX (可选)
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ pack_gui.py — Tkinter GUI for PyInstaller Batch Builder
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import subprocess
9
+ import threading
10
+ from pathlib import Path
11
+ from tkinter import *
12
+ from tkinter.ttk import *
13
+ from tkinter import filedialog # 新增导入
14
+
15
+ class PackerGUI(Frame):
16
+ def __init__(self, root):
17
+ super().__init__(root)
18
+ self.root = root
19
+ self.root.title("PyInstaller 打包工具")
20
+ self.root.geometry("720x520")
21
+
22
+ self.project = Path(__file__).resolve().parent
23
+ self.dist = self.project / "dist"
24
+ self.logs = self.project / "logs"
25
+ self.dist.mkdir(exist_ok=True)
26
+ self.logs.mkdir(exist_ok=True)
27
+
28
+ self.scripts = {}
29
+ self.modes = {}
30
+
31
+ self.build_ui()
32
+ self.load_scripts()
33
+
34
+ # ----------------- UI -----------------
35
+ def build_ui(self):
36
+ # 顶部按钮区域
37
+ top_frame = Frame(self)
38
+ top_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky=W+E)
39
+
40
+ Label(top_frame, text="选择要打包的脚本").pack(side=LEFT, padx=(0, 10))
41
+ Button(top_frame, text="选择py文件", command=self.select_py_file).pack(side=LEFT)
42
+ Button(top_frame, text="刷新列表", command=self.refresh_scripts).pack(side=LEFT, padx=(10, 0))
43
+
44
+ self.list_frame = Frame(self)
45
+ self.list_frame.grid(row=1, column=0, columnspan=2, padx=10, sticky=NSEW)
46
+
47
+ self.log = Text(self, height=12)
48
+ self.log.grid(row=2, column=0, columnspan=2, padx=10, pady=10, sticky=NSEW)
49
+
50
+ self.btn = Button(self, text="开始打包", command=self.start_pack)
51
+ self.btn.grid(row=3, column=0, pady=(0, 10))
52
+
53
+ Button(self, text="打开 dist", command=lambda: os.startfile(self.dist)) \
54
+ .grid(row=3, column=1, pady=(0, 10))
55
+
56
+ self.grid_rowconfigure(1, weight=1)
57
+ self.grid_rowconfigure(2, weight=1)
58
+ self.grid_columnconfigure(0, weight=1)
59
+
60
+ def select_py_file(self):
61
+ """选择py文件"""
62
+ file_path = filedialog.askopenfilename(
63
+ title="选择Python文件",
64
+ filetypes=[("Python files", "*.py"), ("All files", "*.*")]
65
+ )
66
+ if file_path:
67
+ script_path = Path(file_path)
68
+ if script_path not in self.scripts:
69
+ self.add_script_to_list(script_path)
70
+ self.log_write(f"已添加: {script_path.name}")
71
+ else:
72
+ self.log_write(f"已存在: {script_path.name}")
73
+
74
+ def refresh_scripts(self):
75
+ """刷新脚本列表"""
76
+ # 清空现有列表
77
+ for widget in self.list_frame.winfo_children():
78
+ widget.destroy()
79
+ self.scripts.clear()
80
+ self.modes.clear()
81
+
82
+ # 重新加载项目目录下的脚本
83
+ self.load_scripts()
84
+ self.log_write("已刷新脚本列表")
85
+
86
+ def add_script_to_list(self, py_path):
87
+ """将单个脚本添加到列表"""
88
+ var = BooleanVar(value=True)
89
+ mode = BooleanVar(value=False) # False = Console, True = GUI
90
+
91
+ f = Frame(self.list_frame)
92
+ f.pack(fill=X, pady=2)
93
+
94
+ Checkbutton(f, variable=var, width=2).pack(side=LEFT)
95
+ Label(f, text=py_path.name, width=30, anchor=W).pack(side=LEFT)
96
+
97
+ # 显示完整路径(如果是外部文件)
98
+ if py_path.parent != self.project:
99
+ Label(f, text=f"({py_path.parent})", anchor=W).pack(side=LEFT, padx=(5, 0))
100
+
101
+ Checkbutton(f, text="GUI", variable=mode).pack(side=RIGHT)
102
+
103
+ self.scripts[py_path] = var
104
+ self.modes[py_path] = mode
105
+
106
+ def load_scripts(self):
107
+ for py in self.project.glob("*.py"):
108
+ if py.name == "pack_gui.py":
109
+ continue
110
+ self.add_script_to_list(py)
111
+
112
+ # ----------------- Logic -----------------
113
+ def log_write(self, msg):
114
+ self.log.insert(END, msg + "\n")
115
+ self.log.see(END)
116
+
117
+ def start_pack(self):
118
+ self.btn.config(state=DISABLED)
119
+ self.log.delete(1.0, END)
120
+ threading.Thread(target=self.pack_all, daemon=True).start()
121
+
122
+ def pack_all(self):
123
+ for script, checked in self.scripts.items():
124
+ if not checked.get():
125
+ continue
126
+ self.pack_one(script)
127
+
128
+ self.log_write("\n✅ 全部完成")
129
+ self.btn.config(state=NORMAL)
130
+
131
+ def pack_one(self, script: Path):
132
+ self.log_write(f"\n📦 {script.name}")
133
+
134
+ mode_flag = ["--windowed"] if self.modes[script].get() else []
135
+ icon = ["--icon", str(script.with_suffix(".ico"))] \
136
+ if script.with_suffix(".ico").exists() else []
137
+
138
+ # 修改:检查脚本所在目录的assets文件夹
139
+ asset_dir = script.parent / "assets" / script.stem
140
+ assets = ["--add-data", f"{asset_dir};assets/{script.stem}"] \
141
+ if asset_dir.exists() else []
142
+
143
+ log_file = self.logs / f"{script.stem}.log"
144
+
145
+ cmd = [
146
+ sys.executable, "-m", "PyInstaller",
147
+ "-F", "--clean",
148
+ "--name", script.stem,
149
+ *mode_flag,
150
+ *icon,
151
+ *assets,
152
+ str(script)
153
+ ]
154
+
155
+ with open(log_file, "w", encoding="utf-8") as f:
156
+ proc = subprocess.Popen(
157
+ cmd,
158
+ stdout=f,
159
+ stderr=subprocess.STDOUT
160
+ )
161
+
162
+ proc.wait()
163
+
164
+ exe = self.project / "dist" / f"{script.stem}.exe"
165
+ if exe.exists():
166
+ exe.replace(self.dist / exe.name)
167
+ self.log_write("✅ 成功")
168
+ else:
169
+ self.log_write(f"❌ 失败,查看日志: {log_file}")
170
+
171
+ # 清理临时目录
172
+ import shutil
173
+ shutil.rmtree(self.project / "build", ignore_errors=True)
174
+ shutil.rmtree(self.project / script.stem, ignore_errors=True)
175
+
176
+
177
+ if __name__ == "__main__":
178
+ root = Tk()
179
+ app = PackerGUI(root)
180
+ app.pack(fill=BOTH, expand=True)
181
+ root.mainloop()
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ pack_gui.py — Tkinter GUI for PyInstaller Batch Builder
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import subprocess
9
+ import threading
10
+ from pathlib import Path
11
+ from tkinter import *
12
+ from tkinter.ttk import *
13
+
14
+ class PackerGUI(Frame):
15
+ def __init__(self, root):
16
+ super().__init__(root)
17
+ self.root = root
18
+ self.root.title("PyInstaller 打包工具")
19
+ self.root.geometry("720x520")
20
+
21
+ self.project = Path(__file__).resolve().parent
22
+ self.dist = self.project / "dist"
23
+ self.logs = self.project / "logs"
24
+ self.dist.mkdir(exist_ok=True)
25
+ self.logs.mkdir(exist_ok=True)
26
+
27
+ self.scripts = {}
28
+ self.modes = {}
29
+
30
+ self.build_ui()
31
+ self.load_scripts()
32
+
33
+ # ----------------- UI -----------------
34
+ def build_ui(self):
35
+ Label(self, text="选择要打包的脚本").grid(
36
+ row=0, column=0, sticky=W, padx=10, pady=(10, 0)
37
+ )
38
+
39
+ self.list_frame = Frame(self)
40
+ self.list_frame.grid(row=1, column=0, columnspan=2, padx=10, sticky=NSEW)
41
+
42
+ self.log = Text(self, height=12)
43
+ self.log.grid(row=2, column=0, columnspan=2, padx=10, pady=10, sticky=NSEW)
44
+
45
+ self.btn = Button(self, text="开始打包", command=self.start_pack)
46
+ self.btn.grid(row=3, column=0, pady=(0, 10))
47
+
48
+ Button(self, text="打开 dist", command=lambda: os.startfile(self.dist)) \
49
+ .grid(row=3, column=1, pady=(0, 10))
50
+
51
+ self.grid_rowconfigure(1, weight=1)
52
+ self.grid_rowconfigure(2, weight=1)
53
+ self.grid_columnconfigure(0, weight=1)
54
+
55
+ def load_scripts(self):
56
+ for py in self.project.glob("*.py"):
57
+ if py.name == "pack_gui.py":
58
+ continue
59
+
60
+ var = BooleanVar(value=True)
61
+ mode = BooleanVar(value=False) # False = Console, True = GUI
62
+
63
+ f = Frame(self.list_frame)
64
+ f.pack(fill=X, pady=2)
65
+
66
+ Checkbutton(f, variable=var, width=2).pack(side=LEFT)
67
+ Label(f, text=py.name, width=30, anchor=W).pack(side=LEFT)
68
+
69
+ Checkbutton(f, text="GUI", variable=mode).pack(side=RIGHT)
70
+
71
+ self.scripts[py] = var
72
+ self.modes[py] = mode
73
+
74
+ # ----------------- Logic -----------------
75
+ def log_write(self, msg):
76
+ self.log.insert(END, msg + "\n")
77
+ self.log.see(END)
78
+
79
+ def start_pack(self):
80
+ self.btn.config(state=DISABLED)
81
+ self.log.delete(1.0, END)
82
+ threading.Thread(target=self.pack_all, daemon=True).start()
83
+
84
+ def pack_all(self):
85
+ for script, checked in self.scripts.items():
86
+ if not checked.get():
87
+ continue
88
+ self.pack_one(script)
89
+
90
+ self.log_write("\n✅ 全部完成")
91
+ self.btn.config(state=NORMAL)
92
+
93
+ def pack_one(self, script: Path):
94
+ self.log_write(f"\n📦 {script.name}")
95
+
96
+ mode_flag = ["--windowed"] if self.modes[script].get() else []
97
+ icon = ["--icon", str(script.with_suffix(".ico"))] \
98
+ if script.with_suffix(".ico").exists() else []
99
+
100
+ asset_dir = self.project / "assets" / script.stem
101
+ assets = ["--add-data", f"{asset_dir};assets/{script.stem}"] \
102
+ if asset_dir.exists() else []
103
+
104
+ log_file = self.logs / f"{script.stem}.log"
105
+
106
+ cmd = [
107
+ sys.executable, "-m", "PyInstaller",
108
+ "-F", "--clean",
109
+ "--name", script.stem,
110
+ *mode_flag,
111
+ *icon,
112
+ *assets,
113
+ str(script)
114
+ ]
115
+
116
+ with open(log_file, "w", encoding="utf-8") as f:
117
+ proc = subprocess.Popen(
118
+ cmd,
119
+ stdout=f,
120
+ stderr=subprocess.STDOUT
121
+ )
122
+
123
+ proc.wait()
124
+
125
+ exe = self.project / "dist" / f"{script.stem}.exe"
126
+ if exe.exists():
127
+ exe.replace(self.dist / exe.name)
128
+ self.log_write("✅ 成功")
129
+ else:
130
+ self.log_write(f"❌ 失败,查看日志: {log_file}")
131
+
132
+ # 清理临时目录
133
+ import shutil
134
+ shutil.rmtree(self.project / "build", ignore_errors=True)
135
+ shutil.rmtree(self.project / script.stem, ignore_errors=True)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ root = Tk()
140
+ app = PackerGUI(root)
141
+ app.pack(fill=BOTH, expand=True)
142
+ root.mainloop()
@@ -0,0 +1,678 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ packer_pro.py — 强化版 PyInstaller 打包工具
4
+ 支持:单文件/多文件架构、图标内嵌、数字签名
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import subprocess
10
+ import threading
11
+ import shutil
12
+ import json
13
+ import zipfile
14
+ import tempfile
15
+ from pathlib import Path
16
+ from tkinter import *
17
+ from tkinter.ttk import *
18
+ from tkinter import filedialog, messagebox, colorchooser
19
+ import tkinter as tk
20
+
21
+ # ─── 工具函数 ────────────────────────────────────────────────────────────────
22
+
23
+ def find_signtool():
24
+ """查找 signtool.exe(Windows SDK)"""
25
+ candidates = [
26
+ r"C:\Program Files (x86)\Windows Kits\10\bin\x64\signtool.exe",
27
+ r"C:\Program Files (x86)\Windows Kits\10\bin\x86\signtool.exe",
28
+ r"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe",
29
+ ]
30
+ for p in candidates:
31
+ if os.path.exists(p):
32
+ return p
33
+ # 尝试从 PATH 查找
34
+ try:
35
+ result = subprocess.run(["where", "signtool"], capture_output=True, text=True)
36
+ if result.returncode == 0:
37
+ return result.stdout.strip().split("\n")[0]
38
+ except Exception:
39
+ pass
40
+ return None
41
+
42
+
43
+ def png_to_ico(png_path: Path, ico_path: Path, sizes=None):
44
+ """将 PNG 转换为多尺寸 ICO(需要 Pillow)"""
45
+ try:
46
+ from PIL import Image
47
+ except ImportError:
48
+ raise RuntimeError("需要 Pillow 库来转换图片。请运行: pip install Pillow")
49
+
50
+ if sizes is None:
51
+ sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
52
+
53
+ img = Image.open(png_path).convert("RGBA")
54
+ icons = []
55
+ for size in sizes:
56
+ icons.append(img.resize(size, Image.LANCZOS))
57
+
58
+ icons[0].save(ico_path, format="ICO", sizes=[(i.width, i.height) for i in icons])
59
+ return ico_path
60
+
61
+
62
+ def scan_py_files(project_dir: Path):
63
+ """扫描项目目录中的所有 .py 文件"""
64
+ return sorted(project_dir.rglob("*.py"))
65
+
66
+
67
+ def find_entry_candidates(py_files):
68
+ """查找可能的入口文件"""
69
+ candidates = []
70
+ for f in py_files:
71
+ name = f.name.lower()
72
+ if name in ("main.py", "__main__.py", "app.py", "run.py", "start.py"):
73
+ candidates.append(f)
74
+ return candidates
75
+
76
+
77
+ def get_project_tree(project_dir: Path):
78
+ """获取项目目录树(用于展示)"""
79
+ items = []
80
+ for p in sorted(project_dir.iterdir()):
81
+ if p.name.startswith(".") or p.name == "__pycache__":
82
+ continue
83
+ items.append(p)
84
+ return items
85
+
86
+
87
+ # ─── 主 GUI 类 ───────────────────────────────────────────────────────────────
88
+
89
+ class PackerProGUI(Frame):
90
+ def __init__(self, root):
91
+ super().__init__(root)
92
+ self.root = root
93
+ self.root.title("PyInstaller 打包工具 Pro")
94
+ self.root.geometry("900x700")
95
+
96
+ # 状态变量
97
+ self.project_dir = StringVar()
98
+ self.entry_file = StringVar()
99
+ self.icon_path = StringVar()
100
+ self.mode = StringVar(value="single") # single / multi
101
+ self.console_mode = StringVar(value="windowed") # windowed / console
102
+ self.output_mode = StringVar(value="onefile") # onefile / onedir
103
+ self.sign_enabled = BooleanVar(value=False)
104
+ self.cert_path = StringVar()
105
+ self.cert_password = StringVar()
106
+ self.timestamp_url = StringVar(value="http://timestamp.digicert.com")
107
+
108
+ # 附加数据列表
109
+ self.extra_data = [] # [(src, dst), ...]
110
+ self.extra_binary = [] # [(src, dst), ...]
111
+ self.hidden_imports = []
112
+
113
+ self.project_dir_path = None
114
+ self.pack_dir = Path(__file__).resolve().parent / "dist_pro"
115
+ self.pack_dir.mkdir(exist_ok=True)
116
+
117
+ self.build_ui()
118
+
119
+ def build_ui(self):
120
+ notebook = Notebook(self)
121
+ notebook.grid(row=0, column=0, sticky=NSEW, padx=10, pady=10)
122
+
123
+ # Tab 1: 项目配置
124
+ tab1 = Frame(notebook, padding=10)
125
+ notebook.add(tab1, text="项目配置")
126
+ self.build_project_tab(tab1)
127
+
128
+ # Tab 2: 打包选项
129
+ tab2 = Frame(notebook, padding=10)
130
+ notebook.add(tab2, text="打包选项")
131
+ self.build_options_tab(tab2)
132
+
133
+ # Tab 3: 图标 & 签名
134
+ tab3 = Frame(notebook, padding=10)
135
+ notebook.add(tab3, text="图标 & 签名")
136
+ self.build_icon_tab(tab3)
137
+
138
+ # Tab 4: 高级(附加数据等)
139
+ tab4 = Frame(notebook, padding=10)
140
+ notebook.add(tab4, text="高级")
141
+ self.build_advanced_tab(tab4)
142
+
143
+ # 日志区域
144
+ log_frame = LabelFrame(self, text="构建日志", padding=5)
145
+ log_frame.grid(row=1, column=0, sticky=NSEW, padx=10, pady=(0, 10))
146
+
147
+ self.log = Text(log_frame, height=12, wrap=WORD)
148
+ self.log.pack(fill=BOTH, expand=True)
149
+
150
+ # 底部按钮
151
+ btn_frame = Frame(self)
152
+ btn_frame.grid(row=2, column=0, pady=(0, 10), sticky=EW)
153
+
154
+ Button(btn_frame, text="开始打包", command=self.start_pack).pack(side=LEFT, padx=10)
155
+ Button(btn_frame, text="打开输出目录", command=self.open_output).pack(side=LEFT)
156
+ Button(btn_frame, text="清除日志", command=lambda: self.log.delete(1.0, END)).pack(side=LEFT, padx=10)
157
+ Button(btn_frame, text="退出", command=self.root.quit).pack(side=RIGHT, padx=10)
158
+
159
+ self.grid_rowconfigure(1, weight=1)
160
+ self.grid_columnconfigure(0, weight=1)
161
+
162
+ def build_project_tab(self, parent):
163
+ # 模式选择
164
+ mode_frame = LabelFrame(parent, text="打包模式", padding=10)
165
+ mode_frame.pack(fill=X, pady=(0, 10))
166
+
167
+ Radiobutton(mode_frame, text="单文件(一个 .py 文件)", variable=self.mode,
168
+ value="single", command=self.on_mode_change).pack(anchor=W)
169
+ Radiobutton(mode_frame, text="多文件项目(整个项目目录)", variable=self.mode,
170
+ value="multi", command=self.on_mode_change).pack(anchor=W)
171
+
172
+ # 单文件模式:选择 py 文件
173
+ self.single_frame = Frame(parent)
174
+ self.single_frame.pack(fill=X, pady=(0, 10))
175
+
176
+ Label(self.single_frame, text="Python 文件:").grid(row=0, column=0, sticky=W, pady=5)
177
+ Entry(self.single_frame, textvariable=self.entry_file, width=60).grid(row=0, column=1, padx=5)
178
+ Button(self.single_frame, text="浏览...", command=self.browse_py_file).grid(row=0, column=2)
179
+
180
+ # 多文件模式:选择项目目录 + 入口文件
181
+ self.multi_frame = Frame(parent)
182
+
183
+ Label(self.multi_frame, text="项目目录:").grid(row=0, column=0, sticky=W, pady=5)
184
+ Entry(self.multi_frame, textvariable=self.project_dir, width=60).grid(row=0, column=1, padx=5)
185
+ Button(self.multi_frame, text="选择目录...", command=self.browse_project_dir).grid(row=0, column=2)
186
+
187
+ Label(self.multi_frame, text="入口文件:").grid(row=1, column=0, sticky=W, pady=5)
188
+ self.entry_combo = Combobox(self.multi_frame, textvariable=self.entry_file, width=50)
189
+ self.entry_combo.grid(row=1, column=1, sticky=W, padx=5, pady=5)
190
+ Button(self.multi_frame, text="浏览...", command=self.browse_entry_file).grid(row=1, column=2, pady=5)
191
+
192
+ # 项目文件预览
193
+ Label(parent, text="项目文件:").pack(anchor=W, pady=(10, 5))
194
+ self.file_tree = Treeview(parent, height=10)
195
+ self.file_tree.pack(fill=BOTH, expand=True, pady=(0, 10))
196
+
197
+ self.on_mode_change()
198
+
199
+ def build_options_tab(self, parent):
200
+ # 输出模式
201
+ out_frame = LabelFrame(parent, text="输出格式", padding=10)
202
+ out_frame.pack(fill=X, pady=(0, 10))
203
+
204
+ Radiobutton(out_frame, text="单文件 exe(-F / --onefile)", variable=self.output_mode,
205
+ value="onefile").pack(anchor=W)
206
+ Radiobutton(out_frame, text="目录模式(-D / --onedir)", variable=self.output_mode,
207
+ value="onedir").pack(anchor=W)
208
+
209
+ # 窗口模式
210
+ win_frame = LabelFrame(parent, text="窗口模式", padding=10)
211
+ win_frame.pack(fill=X, pady=(0, 10))
212
+
213
+ Radiobutton(win_frame, text="GUI 模式(无控制台窗口,-w)", variable=self.console_mode,
214
+ value="windowed").pack(anchor=W)
215
+ Radiobutton(win_frame, text="控制台模式(显示控制台,-c)", variable=self.console_mode,
216
+ value="console").pack(anchor=W)
217
+
218
+ # 其他选项
219
+ opt_frame = LabelFrame(parent, text="其他选项", padding=10)
220
+ opt_frame.pack(fill=X, pady=(0, 10))
221
+
222
+ self.opt_clean = BooleanVar(value=True)
223
+ self.opt_noupx = BooleanVar(value=False)
224
+ Checkbutton(opt_frame, text="清理临时文件(--clean)", variable=self.opt_clean).pack(anchor=W)
225
+ Checkbutton(opt_frame, text="禁用 UPX(--noupx)", variable=self.opt_noupx).pack(anchor=W)
226
+
227
+ def build_icon_tab(self, parent):
228
+ # 图标设置
229
+ icon_frame = LabelFrame(parent, text="图标设置", padding=10)
230
+ icon_frame.pack(fill=X, pady=(0, 10))
231
+
232
+ Label(icon_frame, text="图标文件 (.ico / .png / .jpg):").grid(row=0, column=0, sticky=W, pady=5)
233
+ Entry(icon_frame, textvariable=self.icon_path, width=50).grid(row=0, column=1, padx=5)
234
+ Button(icon_frame, text="选择...", command=self.browse_icon).grid(row=0, column=2, padx=(5, 0))
235
+ Button(icon_frame, text="清除", command=lambda: self.icon_path.set("")).grid(row=0, column=3, padx=(5, 0))
236
+
237
+ Label(icon_frame, text="提示: PNG/JPG 会自动转换为 ICO 格式").grid(row=1, column=0, columnspan=4, sticky=W, pady=(5, 0))
238
+
239
+ # 签名设置
240
+ sign_frame = LabelFrame(parent, text="数字签名(防止 SmartScreen 拦截)", padding=10)
241
+ sign_frame.pack(fill=X, pady=(0, 10))
242
+
243
+ Checkbutton(sign_frame, text="启用数字签名", variable=self.sign_enabled,
244
+ command=self.on_sign_toggle).grid(row=0, column=0, columnspan=3, sticky=W, pady=(0, 5))
245
+
246
+ self.sign_widgets = []
247
+
248
+ Label(sign_frame, text="证书文件 (.pfx / .p12):").grid(row=1, column=0, sticky=W, pady=2)
249
+ self.cert_entry = Entry(sign_frame, textvariable=self.cert_path, width=40, state=DISABLED)
250
+ self.cert_entry.grid(row=1, column=1, padx=5)
251
+ self.cert_btn = Button(sign_frame, text="选择...", command=self.browse_cert, state=DISABLED)
252
+ self.cert_btn.grid(row=1, column=2, padx=(5, 0))
253
+ self.sign_widgets.extend([self.cert_entry, self.cert_btn])
254
+
255
+ Label(sign_frame, text="证书密码:").grid(row=2, column=0, sticky=W, pady=2)
256
+ self.pwd_entry = Entry(sign_frame, textvariable=self.cert_password, width=40, show="*", state=DISABLED)
257
+ self.pwd_entry.grid(row=2, column=1, padx=5, sticky=W)
258
+ self.sign_widgets.append(self.pwd_entry)
259
+
260
+ Label(sign_frame, text="时间戳服务器:").grid(row=3, column=0, sticky=W, pady=2)
261
+ self.ts_entry = Entry(sign_frame, textvariable=self.timestamp_url, width=40, state=DISABLED)
262
+ self.ts_entry.grid(row=3, column=1, padx=5, sticky=W)
263
+ self.sign_widgets.append(self.ts_entry)
264
+
265
+ # 检测 signtool
266
+ self.signtool_path = find_signtool()
267
+ status_text = f"signtool.exe: {'✅ 已找到' if self.signtool_path else '❌ 未找到(需安装 Windows SDK)'}"
268
+ Label(sign_frame, text=status_text).grid(row=4, column=0, columnspan=3, sticky=W, pady=(5, 0))
269
+
270
+ def build_advanced_tab(self, parent):
271
+ # 附加数据
272
+ data_frame = LabelFrame(parent, text="附加数据(--add-data)", padding=10)
273
+ data_frame.pack(fill=X, pady=(0, 10))
274
+
275
+ self.data_listbox = Listbox(data_frame, height=5)
276
+ self.data_listbox.pack(fill=X, pady=(0, 5))
277
+
278
+ data_btn_frame = Frame(data_frame)
279
+ data_btn_frame.pack(fill=X)
280
+ Button(data_btn_frame, text="添加文件", command=self.add_data_file).pack(side=LEFT, padx=(0, 5))
281
+ Button(data_btn_frame, text="添加目录", command=self.add_data_dir).pack(side=LEFT, padx=5)
282
+ Button(data_btn_frame, text="删除选中", command=self.remove_data).pack(side=LEFT, padx=5)
283
+
284
+ # 附加二进制
285
+ bin_frame = LabelFrame(parent, text="附加二进制(--add-binary)", padding=10)
286
+ bin_frame.pack(fill=X, pady=(0, 10))
287
+
288
+ self.binary_listbox = Listbox(bin_frame, height=5)
289
+ self.binary_listbox.pack(fill=X, pady=(0, 5))
290
+
291
+ bin_btn_frame = Frame(bin_frame)
292
+ bin_btn_frame.pack(fill=X)
293
+ Button(bin_btn_frame, text="添加文件", command=self.add_binary_file).pack(side=LEFT, padx=(0, 5))
294
+ Button(bin_btn_frame, text="删除选中", command=self.remove_binary).pack(side=LEFT, padx=5)
295
+
296
+ # 隐藏导入
297
+ hid_frame = LabelFrame(parent, text="隐藏导入(--hidden-import)", padding=10)
298
+ hid_frame.pack(fill=X, pady=(0, 10))
299
+
300
+ self.hid_entry = Entry(hid_frame)
301
+ self.hid_entry.pack(side=LEFT, fill=X, expand=True, padx=(0, 5))
302
+ Button(hid_frame, text="添加", command=self.add_hidden_import).pack(side=LEFT)
303
+
304
+ self.hid_listbox = Listbox(parent, height=4)
305
+ self.hid_listbox.pack(fill=X, pady=(0, 5))
306
+ Button(parent, text="删除选中隐藏导入", command=self.remove_hidden_import).pack(anchor=W)
307
+
308
+ # ─── 事件处理 ─────────────────────────────────────────────────────────────
309
+
310
+ def on_mode_change(self):
311
+ if self.mode.get() == "single":
312
+ self.single_frame.pack(fill=X, pady=(0, 10))
313
+ self.multi_frame.pack_forget()
314
+ else:
315
+ self.single_frame.pack_forget()
316
+ self.multi_frame.pack(fill=X, pady=(0, 10))
317
+
318
+ def on_sign_toggle(self):
319
+ state = NORMAL if self.sign_enabled.get() else DISABLED
320
+ for w in self.sign_widgets:
321
+ w.config(state=state)
322
+
323
+ def browse_py_file(self):
324
+ path = filedialog.askopenfilename(title="选择 Python 文件", filetypes=[("Python", "*.py")])
325
+ if path:
326
+ self.entry_file.set(path)
327
+
328
+ def browse_project_dir(self):
329
+ path = filedialog.askdirectory(title="选择项目目录")
330
+ if path:
331
+ self.project_dir.set(path)
332
+ self.project_dir_path = Path(path)
333
+ self.refresh_file_tree()
334
+
335
+ def browse_entry_file(self):
336
+ """手动选择入口文件"""
337
+ initial_dir = self.project_dir.get() if self.project_dir.get() else None
338
+ path = filedialog.askopenfilename(title="选择入口文件", filetypes=[("Python", "*.py")],
339
+ initialdir=initial_dir)
340
+ if path:
341
+ # 如果在项目目录内,显示相对路径;否则显示绝对路径
342
+ if self.project_dir_path and str(self.project_dir_path) in path:
343
+ self.entry_file.set(str(Path(path).relative_to(self.project_dir_path)))
344
+ else:
345
+ self.entry_file.set(path)
346
+
347
+ def refresh_file_tree(self):
348
+ self.file_tree.delete(*self.file_tree.get_children())
349
+ if not self.project_dir_path or not self.project_dir_path.exists():
350
+ return
351
+
352
+ py_files = scan_py_files(self.project_dir_path)
353
+ candidates = find_entry_candidates(py_files)
354
+
355
+ # 更新入口文件下拉
356
+ candidate_strs = [str(f.relative_to(self.project_dir_path)) for f in candidates]
357
+ all_py_strs = [str(f.relative_to(self.project_dir_path)) for f in py_files]
358
+
359
+ self.entry_combo["values"] = candidate_strs if candidate_strs else all_py_strs
360
+ if candidate_strs:
361
+ self.entry_combo.set(candidate_strs[0])
362
+
363
+ # 填充树形视图
364
+ def add_items(parent_id, directory):
365
+ for item in sorted(directory.iterdir()):
366
+ if item.name.startswith(".") or item.name == "__pycache__":
367
+ continue
368
+ item_id = self.file_tree.insert(parent_id, END, text=item.name,
369
+ values=(str(item), "📁" if item.is_dir() else "📄"))
370
+ if item.is_dir():
371
+ add_items(item_id, item)
372
+
373
+ add_items("", self.project_dir_path)
374
+ self.file_tree.heading("#0", text=f"项目: {self.project_dir_path.name}")
375
+
376
+ def browse_icon(self):
377
+ path = filedialog.askopenfilename(title="选择图标文件",
378
+ filetypes=[("ICO", "*.ico"), ("PNG", "*.png"), ("JPG", "*.jpg;*.jpeg"), ("All", "*.*")])
379
+ if path:
380
+ self.icon_path.set(path)
381
+
382
+ def browse_cert(self):
383
+ path = filedialog.askopenfilename(title="选择证书文件",
384
+ filetypes=[("PFX", "*.pfx"), ("P12", "*.p12"), ("All", "*.*")])
385
+ if path:
386
+ self.cert_path.set(path)
387
+
388
+ def add_data_file(self):
389
+ path = filedialog.askopenfilename(title="选择要附加的文件")
390
+ if path:
391
+ dst = filedialog.asksaveasfilename(title="目标路径(相对于 exe 目录)", defaultextension="")
392
+ if dst:
393
+ rel_dst = Path(dst).name
394
+ self.extra_data.append((path, rel_dst))
395
+ self.data_listbox.insert(END, f"{Path(path).name} → {rel_dst}")
396
+
397
+ def add_data_dir(self):
398
+ path = filedialog.askdirectory(title="选择要附加的目录")
399
+ if path:
400
+ self.extra_data.append((path, Path(path).name))
401
+ self.data_listbox.insert(END, f"{path} → {Path(path).name}")
402
+
403
+ def remove_data(self):
404
+ sel = self.data_listbox.curselection()
405
+ if sel:
406
+ idx = sel[0]
407
+ self.data_listbox.delete(idx)
408
+ self.extra_data.pop(idx)
409
+
410
+ def add_binary_file(self):
411
+ path = filedialog.askopenfilename(title="选择二进制文件")
412
+ if path:
413
+ self.extra_binary.append((path, Path(path).name))
414
+ self.binary_listbox.insert(END, f"{Path(path).name}")
415
+
416
+ def remove_binary(self):
417
+ sel = self.binary_listbox.curselection()
418
+ if sel:
419
+ idx = sel[0]
420
+ self.binary_listbox.delete(idx)
421
+ self.extra_binary.pop(idx)
422
+
423
+ def add_hidden_import(self):
424
+ val = self.hid_entry.get().strip()
425
+ if val:
426
+ self.hidden_imports.append(val)
427
+ self.hid_listbox.insert(END, val)
428
+ self.hid_entry.delete(0, END)
429
+
430
+ def remove_hidden_import(self):
431
+ sel = self.hid_listbox.curselection()
432
+ if sel:
433
+ idx = sel[0]
434
+ self.hid_listbox.delete(idx)
435
+ self.hidden_imports.pop(idx)
436
+
437
+ # ─── 打包逻辑 ─────────────────────────────────────────────────────────────
438
+
439
+ def resolve_icon(self):
440
+ """处理图标:如果是 png/jpg 则转换为 ico"""
441
+ icon = self.icon_path.get().strip()
442
+ if not icon or not os.path.exists(icon):
443
+ return None
444
+
445
+ icon_p = Path(icon)
446
+ if icon_p.suffix.lower() == ".ico":
447
+ return str(icon_p)
448
+
449
+ # 转换为 ico
450
+ ico_path = icon_p.with_suffix(".ico")
451
+ try:
452
+ png_to_ico(icon_p, ico_path)
453
+ self.log_write(f"图标已转换: {ico_path}")
454
+ return str(ico_path)
455
+ except Exception as e:
456
+ self.log_write(f"⚠ 图标转换失败: {e},跳过图标")
457
+ return None
458
+
459
+ def build_cmd(self, script_path: Path, output_name: str):
460
+ """构建 pyinstaller 命令"""
461
+ cmd = [sys.executable, "-m", "PyInstaller"]
462
+
463
+ # 输出模式
464
+ if self.output_mode.get() == "onefile":
465
+ cmd.append("-F")
466
+ else:
467
+ cmd.append("-D")
468
+
469
+ # 窗口模式
470
+ if self.console_mode.get() == "windowed":
471
+ cmd.append("-w")
472
+ else:
473
+ cmd.append("-c")
474
+
475
+ # 清理
476
+ if self.opt_clean.get():
477
+ cmd.append("--clean")
478
+
479
+ # 禁用 UPX
480
+ if self.opt_noupx.get():
481
+ cmd.append("--noupx")
482
+
483
+ # 输出名称
484
+ cmd.extend(["--name", output_name])
485
+
486
+ # 输出目录
487
+ cmd.extend(["--distpath", str(self.pack_dir)])
488
+ cmd.extend(["--workpath", str(self.pack_dir / "build")])
489
+ cmd.extend(["--specpath", str(self.pack_dir / "spec")])
490
+
491
+ # 图标
492
+ icon = self.resolve_icon()
493
+ if icon:
494
+ cmd.extend(["--icon", icon])
495
+
496
+ # 附加数据
497
+ for src, dst in self.extra_data:
498
+ cmd.extend(["--add-data", f"{src}{os.pathsep}{dst}"])
499
+
500
+ # 附加二进制
501
+ for src, dst in self.extra_binary:
502
+ cmd.extend(["--add-binary", f"{src}{os.pathsep}{dst}"])
503
+
504
+ # 隐藏导入
505
+ for imp in self.hidden_imports:
506
+ cmd.extend(["--hidden-import", imp])
507
+
508
+ # 如果是多文件模式,添加整个项目目录的数据
509
+ if self.mode.get() == "multi" and self.project_dir_path:
510
+ for item in self.project_dir_path.iterdir():
511
+ if item.is_dir() and item.name not in ("__pycache__", ".git", "venv", "env", "dist", "build"):
512
+ rel = item.name
513
+ cmd.extend(["--add-data", f"{item}{os.pathsep}{rel}"])
514
+
515
+ # 脚本路径
516
+ cmd.append(str(script_path))
517
+
518
+ return cmd
519
+
520
+ def get_exe_path(self, output_name: str):
521
+ """获取打包后的 exe 路径"""
522
+ if self.output_mode.get() == "onefile":
523
+ return self.pack_dir / f"{output_name}.exe"
524
+ else:
525
+ return self.pack_dir / output_name / f"{output_name}.exe"
526
+
527
+ def sign_exe(self, exe_path: Path):
528
+ """对 exe 进行数字签名"""
529
+ if not self.sign_enabled.get():
530
+ return True
531
+
532
+ if not self.signtool_path:
533
+ self.log_write("❌ signtool.exe 未找到,跳过签名")
534
+ return False
535
+
536
+ cert = self.cert_path.get().strip()
537
+ if not cert or not os.path.exists(cert):
538
+ self.log_write("❌ 证书文件不存在,跳过签名")
539
+ return False
540
+
541
+ cmd = [self.signtool_path, "sign"]
542
+ cmd.extend(["/f", cert])
543
+
544
+ pwd = self.cert_password.get().strip()
545
+ if pwd:
546
+ cmd.extend(["/p", pwd])
547
+
548
+ ts = self.timestamp_url.get().strip()
549
+ if ts:
550
+ cmd.extend(["/tr", ts])
551
+ cmd.extend(["/td", "SHA256"])
552
+
553
+ cmd.extend(["/fd", "SHA256"])
554
+ cmd.append(str(exe_path))
555
+
556
+ self.log_write(f"签名命令: {' '.join(str(c) for c in cmd)}")
557
+ result = subprocess.run(cmd, capture_output=True, text=True)
558
+
559
+ if result.returncode == 0:
560
+ self.log_write("✅ 签名成功")
561
+ return True
562
+ else:
563
+ self.log_write(f"❌ 签名失败: {result.stderr}")
564
+ return False
565
+
566
+ def start_pack(self):
567
+ """开始打包"""
568
+ # 验证输入
569
+ if not self.entry_file.get().strip():
570
+ messagebox.showerror("错误", "请选择入口文件")
571
+ return
572
+
573
+ entry = Path(self.entry_file.get())
574
+ if not entry.exists():
575
+ messagebox.showerror("错误", f"文件不存在: {entry}")
576
+ return
577
+
578
+ if self.sign_enabled.get():
579
+ if not self.cert_path.get().strip():
580
+ messagebox.showerror("错误", "已启用签名,但未选择证书文件")
581
+ return
582
+
583
+ self.log_write("=" * 60)
584
+ self.log_write(f"开始打包: {entry.name}")
585
+ self.log_write("=" * 60)
586
+
587
+ threading.Thread(target=self.pack_thread, args=(entry,), daemon=True).start()
588
+
589
+ def pack_thread(self, entry: Path):
590
+ """打包线程"""
591
+ output_name = entry.stem
592
+
593
+ # 构建命令
594
+ cmd = self.build_cmd(entry, output_name)
595
+ self.log_write(f"命令: {' '.join(str(c) for c in cmd)}")
596
+ self.log_write("")
597
+
598
+ # 执行打包
599
+ try:
600
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
601
+ text=True, encoding="utf-8", errors="replace")
602
+
603
+ for line in process.stdout:
604
+ self.log_write(line.rstrip())
605
+
606
+ process.wait()
607
+
608
+ if process.returncode != 0:
609
+ self.log_write(f"❌ 打包失败 (returncode={process.returncode})")
610
+ return
611
+
612
+ except Exception as e:
613
+ self.log_write(f"❌ 打包异常: {e}")
614
+ return
615
+
616
+ # 检查输出
617
+ exe_path = self.get_exe_path(output_name)
618
+ if not exe_path.exists():
619
+ # 尝试在 dist_pro 的子目录中查找
620
+ if self.output_mode.get() == "onedir":
621
+ possible = list(self.pack_dir.glob(f"**/{output_name}.exe"))
622
+ if possible:
623
+ exe_path = possible[0]
624
+
625
+ if not exe_path or not exe_path.exists():
626
+ self.log_write(f"❌ 未找到输出的 exe 文件")
627
+ self.log_write(f" 期望路径: {self.get_exe_path(output_name)}")
628
+ return
629
+
630
+ self.log_write(f"✅ 打包成功: {exe_path}")
631
+
632
+ # 数字签名
633
+ if self.sign_enabled.get():
634
+ self.log_write("")
635
+ self.log_write("开始数字签名...")
636
+ self.sign_exe(exe_path)
637
+
638
+ self.log_write("")
639
+ self.log_write("=" * 60)
640
+ self.log_write("🎉 全部完成!")
641
+ self.log_write("=" * 60)
642
+
643
+ # 打开输出目录
644
+ try:
645
+ os.startfile(exe_path.parent)
646
+ except Exception:
647
+ pass
648
+
649
+ def log_write(self, msg):
650
+ """线程安全的日志写入"""
651
+ def _write():
652
+ self.log.insert(END, msg + "\n")
653
+ self.log.see(END)
654
+ self.root.after(0, _write)
655
+
656
+ def open_output(self):
657
+ """打开输出目录"""
658
+ if self.pack_dir.exists():
659
+ os.startfile(self.pack_dir)
660
+ else:
661
+ messagebox.showinfo("提示", "输出目录不存在")
662
+
663
+
664
+ # ─── 入口 ────────────────────────────────────────────────────────────────────
665
+
666
+ if __name__ == "__main__":
667
+ root = Tk()
668
+ # 设置图标(如果存在)
669
+ icon_path = Path(__file__).resolve().parent / "1.ico"
670
+ if icon_path.exists():
671
+ try:
672
+ root.iconbitmap(str(icon_path))
673
+ except Exception:
674
+ pass
675
+
676
+ app = PackerProGUI(root)
677
+ app.pack(fill=BOTH, expand=True)
678
+ root.mainloop()
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyinstaller-tool
3
+ Version: 1.0.0
4
+ Summary: PyInstaller 批量打包工具集(基础/专业/全自动 GUI + 批处理)
5
+ Author: siwpan
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitee.com/siwpan/pyinstaller
8
+ Project-URL: Repository, https://gitee.com/siwpan/pyinstaller
9
+ Project-URL: Changelog, https://gitee.com/siwpan/pyinstaller/blob/master/CHANGELOG.md
10
+ Keywords: pyinstaller,packaging,exe,gui,build
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pyinstaller
15
+ Requires-Dist: Pillow
16
+ Dynamic: license-file
17
+
18
+ # PyInstaller 批量打包工具集
19
+
20
+ > 仓库:<https://gitee.com/siwpan/pyinstaller> | 许可:MIT(见 [LICENSE](LICENSE))
21
+
22
+ 多功能 PyInstaller 批量打包工具箱,从基础到专业级打包。
23
+
24
+ ## 功能
25
+
26
+ - **基础版 (all.py)** — Tkinter GUI,勾选 .py 文件批量打包
27
+ - **专业版 (packer_pro.py)** — 强化 GUI,支持单文件/多文件架构、PNG→ICO、数字签名 (signtool)、附加数据、隐藏导入
28
+ - **全自动 (全宝.bat)** — 自动打包当前目录所有 .py 文件
29
+ - **智能版 (全能宝.bat)** — 自动检测 tkinter/PyQt/PySide 选择 GUI 模式,支持 UPX 压缩
30
+ - **通用打包.bat** — 手动选择单个 .py 打包
31
+ - **图标.bat** — 图标格式转换工具
32
+
33
+ ## 快速开始
34
+
35
+ ```bash
36
+ # 安装依赖
37
+ pip install pyinstaller Pillow
38
+
39
+ # 基础 GUI
40
+ python all.py
41
+
42
+ # 专业 GUI
43
+ python packer_pro.py
44
+
45
+ # 批处理打包
46
+ 全宝.bat
47
+ ```
48
+
49
+ ## 项目结构
50
+
51
+ ```
52
+ pyinstaller/
53
+ ├── all.py # 基础版 GUI
54
+ ├── packer_pro.py # 专业版 GUI (增强功能)
55
+ ├── 全宝.bat # 全自动打包批处理
56
+ ├── 全能宝.bat # 智能打包批处理
57
+ ├── 通用打包.bat # 手动单文件打包
58
+ ├── 图标.bat # 图标转换
59
+ ├── 图标.ico # 默认图标
60
+ └── dist/ # 打包输出
61
+ ```
62
+
63
+ ## 依赖
64
+
65
+ - Python >= 3.8
66
+ - pyinstaller, Pillow (可选), Windows SDK signtool (可选), UPX (可选)
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ afo.py
5
+ all.py
6
+ packer_pro.py
7
+ pyproject.toml
8
+ pyinstaller_tool.egg-info/PKG-INFO
9
+ pyinstaller_tool.egg-info/SOURCES.txt
10
+ pyinstaller_tool.egg-info/dependency_links.txt
11
+ pyinstaller_tool.egg-info/requires.txt
12
+ pyinstaller_tool.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ pyinstaller
2
+ Pillow
@@ -0,0 +1,3 @@
1
+ afo
2
+ all
3
+ packer_pro
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyinstaller-tool"
7
+ version = "1.0.0"
8
+ description = "PyInstaller 批量打包工具集(基础/专业/全自动 GUI + 批处理)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [{ name = "siwpan" }]
13
+ keywords = ["pyinstaller", "packaging", "exe", "gui", "build"]
14
+ dependencies = ["pyinstaller", "Pillow"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://gitee.com/siwpan/pyinstaller"
18
+ Repository = "https://gitee.com/siwpan/pyinstaller"
19
+ Changelog = "https://gitee.com/siwpan/pyinstaller/blob/master/CHANGELOG.md"
20
+
21
+ [tool.setuptools]
22
+ py-modules = ["all", "afo", "packer_pro"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+