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.
- pyinstaller_tool-1.0.0/LICENSE +21 -0
- pyinstaller_tool-1.0.0/MANIFEST.in +14 -0
- pyinstaller_tool-1.0.0/PKG-INFO +66 -0
- pyinstaller_tool-1.0.0/README.md +49 -0
- pyinstaller_tool-1.0.0/afo.py +181 -0
- pyinstaller_tool-1.0.0/all.py +142 -0
- pyinstaller_tool-1.0.0/packer_pro.py +678 -0
- pyinstaller_tool-1.0.0/pyinstaller_tool.egg-info/PKG-INFO +66 -0
- pyinstaller_tool-1.0.0/pyinstaller_tool.egg-info/SOURCES.txt +12 -0
- pyinstaller_tool-1.0.0/pyinstaller_tool.egg-info/dependency_links.txt +1 -0
- pyinstaller_tool-1.0.0/pyinstaller_tool.egg-info/requires.txt +2 -0
- pyinstaller_tool-1.0.0/pyinstaller_tool.egg-info/top_level.txt +3 -0
- pyinstaller_tool-1.0.0/pyproject.toml +22 -0
- pyinstaller_tool-1.0.0/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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"]
|