Skip to content

Commit 297b5a9

Browse files
authored
Add files via upload
Скрипт PyInstallerGUI-NZ для конвертирования скриптов, файлов (.py) в исполняемый файл (.exe), с функцией сжатия NanoZip и создание и запуск EXE без консоли.
1 parent 838e5b5 commit 297b5a9

9 files changed

Lines changed: 319 additions & 2 deletions

File tree

PyInstallerGUI-NZ.py

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import sys
2+
import os
3+
import shutil
4+
import subprocess
5+
import re
6+
from PyQt5.QtWidgets import (
7+
QApplication, QWidget, QLabel, QLineEdit, QPushButton,
8+
QTextEdit, QCheckBox, QFileDialog, QVBoxLayout, QHBoxLayout
9+
)
10+
11+
class PyInstallerGUI(QWidget):
12+
def __init__(self):
13+
super().__init__()
14+
self.init_ui()
15+
16+
def init_ui(self):
17+
self.setWindowTitle("PyInstallerGUI+Nanozip-SFX")
18+
19+
# Метки и поля ввода
20+
self.label_py = QLabel("Python файл (*.py):")
21+
self.line_py = QLineEdit()
22+
self.btn_py = QPushButton("Выбрать")
23+
self.btn_py.clicked.connect(self.select_py_file)
24+
25+
self.label_out = QLabel("Папка вывода:")
26+
self.line_out = QLineEdit()
27+
self.btn_out = QPushButton("Выбрать")
28+
self.btn_out.clicked.connect(self.select_out_folder)
29+
30+
self.label_icon = QLabel("Иконка (*.ico):")
31+
self.line_icon = QLineEdit()
32+
self.btn_icon = QPushButton("Выбрать")
33+
self.btn_icon.clicked.connect(self.select_icon_file)
34+
35+
self.checkbox_compress = QCheckBox("Использовать сжатие nanozip + SFX")
36+
37+
self.checkbox_no_console = QCheckBox("Создавать EXE без консоли (GUI-приложение)")
38+
39+
self.btn_run = QPushButton("Собрать EXE")
40+
self.btn_run.clicked.connect(self.run_pyinstaller)
41+
42+
self.log = QTextEdit()
43+
self.log.setReadOnly(True)
44+
45+
layout = QVBoxLayout()
46+
47+
h_py = QHBoxLayout()
48+
h_py.addWidget(self.line_py)
49+
h_py.addWidget(self.btn_py)
50+
layout.addWidget(self.label_py)
51+
layout.addLayout(h_py)
52+
53+
h_out = QHBoxLayout()
54+
h_out.addWidget(self.line_out)
55+
h_out.addWidget(self.btn_out)
56+
layout.addWidget(self.label_out)
57+
layout.addLayout(h_out)
58+
59+
h_icon = QHBoxLayout()
60+
h_icon.addWidget(self.line_icon)
61+
h_icon.addWidget(self.btn_icon)
62+
layout.addWidget(self.label_icon)
63+
layout.addLayout(h_icon)
64+
65+
layout.addWidget(self.checkbox_compress)
66+
layout.addWidget(self.checkbox_no_console)
67+
layout.addWidget(self.btn_run)
68+
layout.addWidget(self.log)
69+
70+
self.setLayout(layout)
71+
self.resize(700, 500)
72+
73+
def select_py_file(self):
74+
path, _ = QFileDialog.getOpenFileName(self, "Выбрать Python файл", "", "Python Files (*.py)")
75+
if path:
76+
self.line_py.setText(path)
77+
78+
def select_out_folder(self):
79+
path = QFileDialog.getExistingDirectory(self, "Выбрать папку вывода")
80+
if path:
81+
self.line_out.setText(path)
82+
83+
def select_icon_file(self):
84+
path, _ = QFileDialog.getOpenFileName(self, "Выбрать иконку", "", "Icon Files (*.ico)")
85+
if path:
86+
self.line_icon.setText(path)
87+
88+
def run_process(self, cmd, name):
89+
self.log.append(f"Запуск: {name}{' '.join(cmd)}")
90+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
91+
for line in process.stdout:
92+
self.log.append(line.rstrip())
93+
process.wait()
94+
if process.returncode != 0:
95+
self.log.append(f"Ошибка: {name} завершился с кодом {process.returncode}")
96+
return False
97+
self.log.append(f"{name} завершён успешно.")
98+
return True
99+
100+
def collect_extra_files(self, script_path):
101+
base_dir = os.path.dirname(os.path.abspath(script_path))
102+
extra_paths = []
103+
104+
for item in os.listdir(base_dir):
105+
full_path = os.path.join(base_dir, item)
106+
if os.path.abspath(full_path) == os.path.abspath(script_path):
107+
continue
108+
if os.path.isdir(full_path):
109+
extra_paths.append(f"{full_path};{item}")
110+
else:
111+
extra_paths.append(f"{full_path};.")
112+
113+
# Добавляем папку tkthemes, если есть
114+
tkthemes_path = os.path.join(base_dir, "tkthemes")
115+
if os.path.isdir(tkthemes_path):
116+
extra_paths.append(f"{tkthemes_path};tkthemes")
117+
118+
return extra_paths
119+
120+
def prepare_script_for_build(self, orig_path):
121+
"""
122+
Создаёт временную копию скрипта с вставленной функцией resource_path,
123+
подменой open() на open(resource_path()), и добавлением os.chdir(sys._MEIPASS)
124+
при запуске из frozen.
125+
"""
126+
127+
with open(orig_path, "r", encoding="utf-8") as f:
128+
src = f.read()
129+
130+
# Функция resource_path + переключение cwd, вставляем в начало
131+
patch_code = '''
132+
import sys
133+
import os
134+
135+
def resource_path(relative_path):
136+
if hasattr(sys, '_MEIPASS'):
137+
return os.path.join(sys._MEIPASS, relative_path)
138+
return os.path.join(os.path.abspath("."), relative_path)
139+
140+
if getattr(sys, 'frozen', False):
141+
os.chdir(sys._MEIPASS)
142+
'''
143+
144+
# Заменяем open("...") и open('...') на open(resource_path("..."))
145+
# Регулярное выражение ищет open("somepath") или open('somepath')
146+
pattern = re.compile(r'''open\s*\(\s*(['"])(.+?)\1''')
147+
148+
def replacer(m):
149+
quote = m.group(1)
150+
path_inside = m.group(2)
151+
# Если уже используется resource_path, не трогаем
152+
if "resource_path" in m.group(0):
153+
return m.group(0)
154+
return f'open(resource_path({quote}{path_inside}{quote})'
155+
156+
patched_src = pattern.sub(replacer, src)
157+
158+
# Собираем итоговый код
159+
final_src = patch_code + "\n" + patched_src
160+
161+
# Запишем во временный файл рядом с оригиналом
162+
tmp_path = orig_path + "_patched.py"
163+
with open(tmp_path, "w", encoding="utf-8") as f:
164+
f.write(final_src)
165+
166+
return tmp_path
167+
168+
def run_pyinstaller(self):
169+
py_file = self.line_py.text().strip()
170+
out_dir = self.line_out.text().strip()
171+
icon_file = self.line_icon.text().strip()
172+
use_compress = self.checkbox_compress.isChecked()
173+
no_console = self.checkbox_no_console.isChecked()
174+
175+
if not os.path.isfile(py_file):
176+
self.log.append("Ошибка: выбранный Python файл не существует")
177+
return
178+
if not os.path.isdir(out_dir):
179+
self.log.append("Ошибка: выбрана несуществующая папка для результата")
180+
return
181+
if icon_file and not os.path.isfile(icon_file):
182+
self.log.append("Ошибка: выбранный файл иконки не существует")
183+
return
184+
185+
nz_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "nz.exe")
186+
187+
if use_compress:
188+
if not os.path.isfile(nz_path) or not os.access(nz_path, os.X_OK):
189+
self.log.append("Ошибка: nz.exe не найден или не исполняемый рядом со скриптом")
190+
return
191+
192+
self.log.append(f"Запуск сборки для: {py_file}")
193+
self.log.append(f"Вывод в: {out_dir}")
194+
if icon_file:
195+
self.log.append(f"Используется иконка: {icon_file}")
196+
if use_compress:
197+
self.log.append(f"Используется сжатие nanozip + создание SFX")
198+
if no_console:
199+
self.log.append("Сборка EXE без консоли (--noconsole)")
200+
201+
workpath = os.path.join(out_dir, "build")
202+
specpath = os.path.join(out_dir, "spec")
203+
204+
os.makedirs(workpath, exist_ok=True)
205+
os.makedirs(specpath, exist_ok=True)
206+
207+
extra_data = self.collect_extra_files(py_file)
208+
209+
# Патчим исходный скрипт
210+
tmp_py = self.prepare_script_for_build(py_file)
211+
212+
cmd = [
213+
sys.executable, "-m", "PyInstaller",
214+
"--clean",
215+
"--onefile",
216+
f"--distpath={out_dir}",
217+
f"--workpath={workpath}",
218+
f"--specpath={specpath}",
219+
]
220+
221+
if no_console:
222+
cmd.append("--noconsole")
223+
224+
if icon_file:
225+
cmd.append(f"--icon={icon_file}")
226+
227+
for data in extra_data:
228+
cmd.append(f"--add-data={data}")
229+
230+
cmd.append(tmp_py)
231+
232+
self.log.append("Команда PyInstaller: " + " ".join(cmd))
233+
self.btn_run.setEnabled(False)
234+
235+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
236+
for line in process.stdout:
237+
self.log.append(line.rstrip())
238+
process.wait()
239+
240+
# Удаляем временный файл
241+
try:
242+
os.remove(tmp_py)
243+
except Exception as e:
244+
self.log.append(f"Не удалось удалить временный файл: {e}")
245+
246+
if process.returncode != 0:
247+
self.log.append(f"Ошибка сборки PyInstaller. Код выхода: {process.returncode}")
248+
self.btn_run.setEnabled(True)
249+
return
250+
self.log.append("Сборка PyInstaller завершена успешно.")
251+
252+
exe_name = os.path.splitext(os.path.basename(py_file))[0] + ".exe"
253+
exe_path = os.path.join(out_dir, exe_name)
254+
sfx_path = os.path.join(out_dir, "sfx_" + exe_name)
255+
256+
if use_compress:
257+
nz_cmd = [
258+
nz_path, "w32c",
259+
"-cO",
260+
"-m.5g",
261+
"-o", sfx_path,
262+
exe_path
263+
]
264+
if not self.run_process(nz_cmd, "nanozip SFX"):
265+
self.btn_run.setEnabled(True)
266+
return
267+
268+
try:
269+
os.replace(sfx_path, exe_path)
270+
self.log.append(f"SFX создан и заменён: {exe_path}")
271+
except Exception as e:
272+
self.log.append(f"Ошибка замены EXE на SFX: {e}")
273+
self.btn_run.setEnabled(True)
274+
return
275+
276+
try:
277+
if os.path.exists(workpath):
278+
shutil.rmtree(workpath)
279+
self.log.append(f"Папка сборки '{workpath}' удалена.")
280+
if os.path.exists(specpath):
281+
shutil.rmtree(specpath)
282+
self.log.append(f"Папка spec '{specpath}' удалена.")
283+
except Exception as e:
284+
self.log.append(f"Ошибка при удалении временных папок: {e}")
285+
286+
self.log.append("Все операции завершены.")
287+
self.btn_run.setEnabled(True)
288+
289+
290+
if __name__ == "__main__":
291+
app = QApplication(sys.argv)
292+
window = PyInstallerGUI()
293+
window.show()
294+
sys.exit(app.exec())

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,23 @@
1-
# PyInstallerGUI-NZ
2-
Скрипт PyInstallerGUI-NZ для конвертирования скриптов, файлов (.py) в исполняемый файл (.exe)
1+
<img src="https://github.com/Xprograms89/PyInstallerGUI-NZ/blob/main/Work.png" width="702">
2+
3+
# Скрипт PyInstallerGUI-NZ для конвертирования скриптов, файлов (.py) в исполняемый файл (.exe), с функцией сжатия NanoZip и создание и запуск EXE без консоли.
4+
5+
Скрипт PyInstallerGUI-NZ собирает в EXE весь ваш проект\
6+
со всеми необходимыми для запуска файлами, папками и модулями из папки вашего проекта и из site-packages\
7+
Имеет для удобства графический интерфейс на PyQt5\
8+
Работает быстро на основе pyinstaller 6.14.2\
9+
Выполняет сжатие исполняемого файла в NanoZip\
10+
11+
12+
13+
### To english
14+
The PyInstallerGUI-NZ script assembles your entire project into an EXE\
15+
with all the files, folders and modules from site-packages necessary for launch\
16+
It has a PyQt5 graphical interface for convenience\
17+
It works quickly based on pyinstaller 6.14.2\
18+
It compresses the executable file in NanoZip\
19+
20+
## Donate
21+
* **Bitcoin:** `bc1qngs6nyzh9mpcgn5f3vpsqqg90m4evxqxpk4gwr`
22+
* **Ethereum:** `0x846C0F2857743309B43DB4f7Ba7db91aaAfd7E55`
23+
* **Dogecoin:** `DQyXW6EM2ajNX5jseAar3vYEv3hyi7xWUf`

Work.png

22.4 KB
Loading

icon-console.ico

58.1 KB
Binary file not shown.

icon-windowed.ico

59.3 KB
Binary file not shown.

mingwm10.dll

17.8 KB
Binary file not shown.

nz.exe

249 KB
Binary file not shown.

nz_w32c.sfx

120 KB
Binary file not shown.

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pyinstaller
2+
PyQt5

0 commit comments

Comments
 (0)