new
This commit is contained in:
589
build.py
Normal file
589
build.py
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Скрипт для сборки 32-битного приложения в один файл
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class OneFileBuilder:
|
||||||
|
"""Класс для сборки одного исполняемого файла"""
|
||||||
|
|
||||||
|
def __init__(self, config_file='onefile_32bit.spec'):
|
||||||
|
self.config_file = config_file
|
||||||
|
self.project_root = Path.cwd()
|
||||||
|
self.dist_dir = self.project_root / 'dist'
|
||||||
|
self.build_dir = self.project_root / 'build'
|
||||||
|
|
||||||
|
def print_banner(self):
|
||||||
|
"""Вывод информационного баннера"""
|
||||||
|
print("=" * 60)
|
||||||
|
print(" СБОРКА 32-БИТНОГО ПРИЛОЖЕНИЯ В ОДИН ФАЙЛ")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Проект: {self.project_root.name}")
|
||||||
|
print(f"Система: {platform.system()} {platform.architecture()[0]}")
|
||||||
|
print(f"Python: {platform.python_version()}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
def check_requirements(self):
|
||||||
|
"""Проверка необходимых компонентов"""
|
||||||
|
print("\n🔍 Проверка требований...")
|
||||||
|
|
||||||
|
# Проверка Python архитектуры
|
||||||
|
if platform.system() == 'Windows' and platform.architecture()[0] != '32bit':
|
||||||
|
print("⚠️ ВНИМАНИЕ: Используется 64-битный Python!")
|
||||||
|
print("Для сборки 32-битного EXE рекомендуется:")
|
||||||
|
print("1. Установить 32-битный Python с python.org")
|
||||||
|
print("2. Используйте эту версию Python для сборки")
|
||||||
|
response = input("Продолжить сборку? (y/n): ")
|
||||||
|
if response.lower() != 'y':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверка PyInstaller
|
||||||
|
try:
|
||||||
|
import PyInstaller
|
||||||
|
print(f"✓ PyInstaller {PyInstaller.__version__}")
|
||||||
|
except ImportError:
|
||||||
|
print("✗ PyInstaller не установлен")
|
||||||
|
print("Установка: pip install pyinstaller")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверка иконки (не обязательна)
|
||||||
|
icon_paths = ['icon.ico', 'assets/icon.ico', 'images/icon.ico']
|
||||||
|
self.icon_file = None
|
||||||
|
|
||||||
|
for icon in icon_paths:
|
||||||
|
if Path(icon).exists():
|
||||||
|
self.icon_file = icon
|
||||||
|
print(f"✓ Иконка найдена: {icon}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not self.icon_file:
|
||||||
|
print("⚠️ Иконка не найдена. Будет использована стандартная иконка.")
|
||||||
|
|
||||||
|
# Проверка основного файла
|
||||||
|
main_files = ['app.py']
|
||||||
|
self.main_file = None
|
||||||
|
|
||||||
|
for main in main_files:
|
||||||
|
if Path(main).exists():
|
||||||
|
self.main_file = main
|
||||||
|
print(f"✓ Основной файл: {main}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not self.main_file:
|
||||||
|
print("✗ Основной файл не найден!")
|
||||||
|
print("Создайте main.py или укажите другой файл через --main")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setup_environment(self):
|
||||||
|
"""Настройка окружения для 32-битной сборки"""
|
||||||
|
print("\n⚙️ Настройка окружения...")
|
||||||
|
|
||||||
|
# Установка переменных окружения для Windows
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
os.environ['PYINSTALLER_FORCE_32BIT'] = '1'
|
||||||
|
print("✓ Установлена переменная PYINSTALLER_FORCE_32BIT=1")
|
||||||
|
|
||||||
|
# Создание папок если нужно
|
||||||
|
self.dist_dir.mkdir(exist_ok=True)
|
||||||
|
print(f"✓ Папка dist готова")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def generate_version_info(self):
|
||||||
|
"""Генерация файла с информацией о версии"""
|
||||||
|
version_content = f"""# UTF-8
|
||||||
|
VSVersionInfo(
|
||||||
|
ffi=FixedFileInfo(
|
||||||
|
filevers=({self.version_major}, {self.version_minor}, {self.version_patch}, {self.version_build}),
|
||||||
|
prodvers=({self.version_major}, {self.version_minor}, {self.version_patch}, {self.version_build}),
|
||||||
|
mask=0x3f,
|
||||||
|
flags=0x0,
|
||||||
|
OS=0x40004,
|
||||||
|
fileType=0x1,
|
||||||
|
subtype=0x0,
|
||||||
|
date=(0, 0)
|
||||||
|
),
|
||||||
|
kids=[
|
||||||
|
StringFileInfo([
|
||||||
|
StringTable(
|
||||||
|
'040904B0',
|
||||||
|
[
|
||||||
|
StringStruct('CompanyName', '{self.company_name}'),
|
||||||
|
StringStruct('FileDescription', '{self.file_description}'),
|
||||||
|
StringStruct('FileVersion', '{self.version_major}.{self.version_minor}.{self.version_patch}.{self.version_build}'),
|
||||||
|
StringStruct('InternalName', '{self.app_name}'),
|
||||||
|
StringStruct('LegalCopyright', '{self.copyright}'),
|
||||||
|
StringStruct('OriginalFilename', '{self.app_name}.exe'),
|
||||||
|
StringStruct('ProductName', '{self.product_name}'),
|
||||||
|
StringStruct('ProductVersion', '{self.version_major}.{self.version_minor}.{self.version_patch}'),
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
VarFileInfo([VarStruct('Translation', [1033, 1200])])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
version_file = Path('version_info.txt')
|
||||||
|
with open(version_file, 'w', encoding='utf-8-sig') as f:
|
||||||
|
f.write(version_content)
|
||||||
|
|
||||||
|
print("✓ Создан version_info.txt")
|
||||||
|
return version_file
|
||||||
|
|
||||||
|
def build_with_pyinstaller(self, clean_build=True):
|
||||||
|
"""Сборка с помощью PyInstaller"""
|
||||||
|
print(f"\n🔨 Сборка приложения...")
|
||||||
|
|
||||||
|
# Очистка предыдущих сборок
|
||||||
|
if clean_build:
|
||||||
|
if self.build_dir.exists():
|
||||||
|
shutil.rmtree(self.build_dir, ignore_errors=True)
|
||||||
|
print("✓ Очищена папка build")
|
||||||
|
|
||||||
|
# Команда сборки
|
||||||
|
cmd = [
|
||||||
|
sys.executable, '-m', 'PyInstaller',
|
||||||
|
'--onefile', # Один файл
|
||||||
|
'--clean', # Очистка кэша
|
||||||
|
'--noconfirm', # Не спрашивать подтверждение
|
||||||
|
]
|
||||||
|
|
||||||
|
# Добавление иконки если есть
|
||||||
|
if self.icon_file and Path(self.icon_file).exists():
|
||||||
|
cmd.extend(['--icon', self.icon_file])
|
||||||
|
print(f"✓ Используется иконка: {self.icon_file}")
|
||||||
|
else:
|
||||||
|
print("⚠️ Иконка не используется")
|
||||||
|
|
||||||
|
# Добавление информации о версии (только Windows)
|
||||||
|
version_file = Path('version_info.txt')
|
||||||
|
if platform.system() == 'Windows' and version_file.exists():
|
||||||
|
cmd.extend(['--version-file', 'version_info.txt'])
|
||||||
|
print("✓ Используется информация о версии")
|
||||||
|
|
||||||
|
# Имя приложения
|
||||||
|
cmd.extend(['--name', self.app_name])
|
||||||
|
|
||||||
|
# Скрыть консоль для GUI приложений
|
||||||
|
if not self.show_console:
|
||||||
|
cmd.append('--noconsole')
|
||||||
|
cmd.append('--windowed')
|
||||||
|
|
||||||
|
# Скрытые импорты
|
||||||
|
for imp in self.hidden_imports:
|
||||||
|
cmd.extend(['--hidden-import', imp])
|
||||||
|
|
||||||
|
# Добавление данных
|
||||||
|
for data_src, data_dst in self.data_files:
|
||||||
|
cmd.extend(['--add-data', f'{data_src}{os.pathsep}{data_dst}'])
|
||||||
|
|
||||||
|
# Добавление основного файла
|
||||||
|
cmd.append(str(self.main_file))
|
||||||
|
|
||||||
|
# Вывод команды
|
||||||
|
print(f"Команда: {' '.join(cmd)}")
|
||||||
|
print("\n🔄 Запуск PyInstaller...")
|
||||||
|
|
||||||
|
# Выполнение с правильной кодировкой для Windows
|
||||||
|
try:
|
||||||
|
# Для Windows используем кодировку cp1251 или текущую системную
|
||||||
|
encoding = 'cp866' if platform.system() == 'Windows' else 'utf-8'
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding=encoding,
|
||||||
|
errors='replace' # Игнорировать ошибки декодирования
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n✅ Сборка завершена успешно!")
|
||||||
|
|
||||||
|
if result.stdout and result.stdout.strip():
|
||||||
|
print(f"\nВывод PyInstaller:\n{result.stdout}")
|
||||||
|
|
||||||
|
if result.stderr and result.stderr.strip():
|
||||||
|
print(f"\nПредупреждения PyInstaller:\n{result.stderr}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"\n❌ Ошибка сборки!")
|
||||||
|
print(f"Код ошибки: {e.returncode}")
|
||||||
|
|
||||||
|
# Вывод с правильной кодировкой
|
||||||
|
if e.stdout:
|
||||||
|
print(f"\nВывод PyInstaller:\n{e.stdout}")
|
||||||
|
if e.stderr:
|
||||||
|
print(f"\nОшибки PyInstaller:\n{e.stderr}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Неожиданная ошибка: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def build_with_pyinstaller_simple(self):
|
||||||
|
"""Упрощенный метод сборки без перехвата вывода"""
|
||||||
|
print(f"\n🔨 Сборка приложения (упрощенный метод)...")
|
||||||
|
|
||||||
|
# Очистка предыдущих сборок
|
||||||
|
if self.build_dir.exists():
|
||||||
|
shutil.rmtree(self.build_dir, ignore_errors=True)
|
||||||
|
print("✓ Очищена папка build")
|
||||||
|
|
||||||
|
# Базовые аргументы
|
||||||
|
args = [
|
||||||
|
sys.executable, '-m', 'PyInstaller',
|
||||||
|
'--onefile',
|
||||||
|
'--clean',
|
||||||
|
'--noconfirm',
|
||||||
|
'--name', self.app_name,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Дополнительные аргументы
|
||||||
|
if not self.show_console:
|
||||||
|
args.extend(['--noconsole', '--windowed'])
|
||||||
|
|
||||||
|
if self.icon_file and Path(self.icon_file).exists():
|
||||||
|
args.extend(['--icon', self.icon_file])
|
||||||
|
|
||||||
|
version_file = Path('version_info.txt')
|
||||||
|
if platform.system() == 'Windows' and version_file.exists():
|
||||||
|
args.extend(['--version-file', 'version_info.txt'])
|
||||||
|
|
||||||
|
# Добавить main.py
|
||||||
|
args.append(str(self.main_file))
|
||||||
|
|
||||||
|
print(f"Запуск: {' '.join(args)}")
|
||||||
|
|
||||||
|
# Прямой запуск
|
||||||
|
try:
|
||||||
|
return_code = subprocess.call(args)
|
||||||
|
if return_code == 0:
|
||||||
|
print("\n✅ Сборка завершена успешно!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"\n❌ Ошибка сборки. Код возврата: {return_code}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Ошибка: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def optimize_with_upx(self):
|
||||||
|
"""Оптимизация с помощью UPX"""
|
||||||
|
print("\n⚡ Оптимизация с UPX...")
|
||||||
|
|
||||||
|
exe_file = self.dist_dir / f'{self.app_name}.exe'
|
||||||
|
|
||||||
|
if not exe_file.exists():
|
||||||
|
print("✗ Исполняемый файл не найден")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверка наличия UPX
|
||||||
|
try:
|
||||||
|
subprocess.run(['upx', '--version'],
|
||||||
|
capture_output=True, check=True,
|
||||||
|
shell=True) # shell=True для Windows
|
||||||
|
print("✓ UPX найден")
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
print("⚠️ UPX не установлен. Пропуск оптимизации.")
|
||||||
|
print("Скачайте UPX: https://upx.github.io/")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Оптимизация
|
||||||
|
original_size = exe_file.stat().st_size / (1024 * 1024)
|
||||||
|
|
||||||
|
cmd = ['upx', '--best', '--lzma', str(exe_file)]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, check=True, capture_output=True, shell=True)
|
||||||
|
|
||||||
|
new_size = exe_file.stat().st_size / (1024 * 1024)
|
||||||
|
reduction = ((original_size - new_size) / original_size) * 100
|
||||||
|
|
||||||
|
print(f"✓ Оптимизация завершена!")
|
||||||
|
print(f"Размер: {original_size:.2f} MB → {new_size:.2f} MB")
|
||||||
|
print(f"Сжатие: {reduction:.1f}%")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"⚠️ Ошибка оптимизации UPX: {e}")
|
||||||
|
if e.stderr:
|
||||||
|
print(f"Ошибка: {e.stderr.decode('utf-8', errors='replace')}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_executable(self):
|
||||||
|
"""Тестирование исполняемого файла"""
|
||||||
|
print("\n🧪 Тестирование приложения...")
|
||||||
|
|
||||||
|
exe_path = self.dist_dir / f'{self.app_name}.exe'
|
||||||
|
|
||||||
|
if not exe_path.exists():
|
||||||
|
# Проверим другие возможные расширения
|
||||||
|
for ext in ['.exe', '']:
|
||||||
|
test_path = self.dist_dir / f'{self.app_name}{ext}'
|
||||||
|
if test_path.exists():
|
||||||
|
exe_path = test_path
|
||||||
|
break
|
||||||
|
|
||||||
|
if not exe_path.exists():
|
||||||
|
print("✗ Исполняемый файл не найден")
|
||||||
|
print(f"Искали в: {self.dist_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
size_mb = exe_path.stat().st_size / (1024 * 1024)
|
||||||
|
print(f"✓ Файл создан: {exe_path.name}")
|
||||||
|
print(f" Размер: {size_mb:.2f} MB")
|
||||||
|
print(f" Путь: {exe_path}")
|
||||||
|
|
||||||
|
# Проверка архитектуры через pefile если доступен
|
||||||
|
try:
|
||||||
|
import pefile
|
||||||
|
pe = pefile.PE(str(exe_path))
|
||||||
|
machine = pe.FILE_HEADER.Machine
|
||||||
|
arch_map = {
|
||||||
|
0x14c: '32-bit (x86)',
|
||||||
|
0x8664: '64-bit (x64)',
|
||||||
|
}
|
||||||
|
arch_text = arch_map.get(machine, f'Unknown ({hex(machine)})')
|
||||||
|
print(f" Архитектура: {arch_text}")
|
||||||
|
print(f" Секций: {pe.FILE_HEADER.NumberOfSections}")
|
||||||
|
except ImportError:
|
||||||
|
print(" ℹ️ Установите pefile для детальной информации: pip install pefile")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ℹ️ Не удалось проверить архитектуру: {e}")
|
||||||
|
|
||||||
|
# Спросить о запуске теста
|
||||||
|
if self.auto_test:
|
||||||
|
response = 'y'
|
||||||
|
else:
|
||||||
|
response = input("\nЗапустить приложение для теста? (y/n): ")
|
||||||
|
|
||||||
|
if response.lower() == 'y':
|
||||||
|
print(f"Запуск {exe_path.name}...")
|
||||||
|
try:
|
||||||
|
subprocess.Popen([str(exe_path)],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
print("✓ Приложение запущено")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Ошибка запуска: {e}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Очистка временных файлов"""
|
||||||
|
print("\n🧹 Очистка временных файлов...")
|
||||||
|
|
||||||
|
# Удаляем только временные файлы, созданные скриптом
|
||||||
|
temp_files = [
|
||||||
|
'version_info.txt',
|
||||||
|
f'{self.app_name}.spec',
|
||||||
|
]
|
||||||
|
|
||||||
|
for temp_file in temp_files:
|
||||||
|
temp_path = Path(temp_file)
|
||||||
|
if temp_path.exists():
|
||||||
|
try:
|
||||||
|
temp_path.unlink()
|
||||||
|
print(f"✓ Удален {temp_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Не удалось удалить {temp_file}: {e}")
|
||||||
|
|
||||||
|
print("✓ Очистка завершена")
|
||||||
|
|
||||||
|
def build(self, args):
|
||||||
|
"""Основной процесс сборки"""
|
||||||
|
self.print_banner()
|
||||||
|
|
||||||
|
# Настройка параметров
|
||||||
|
self.app_name = args.name or 'MyApp'
|
||||||
|
self.company_name = args.company or 'My Company'
|
||||||
|
self.product_name = args.product or self.app_name
|
||||||
|
self.file_description = args.description or f'{self.app_name} Application'
|
||||||
|
self.copyright = args.copyright or f'Copyright © 2024 {self.company_name}'
|
||||||
|
|
||||||
|
self.version_major = args.version_major or 1
|
||||||
|
self.version_minor = args.version_minor or 0
|
||||||
|
self.version_patch = args.version_patch or 0
|
||||||
|
self.version_build = args.version_build or 0
|
||||||
|
|
||||||
|
self.show_console = args.console
|
||||||
|
self.auto_test = args.test
|
||||||
|
self.hidden_imports = args.hidden_import or []
|
||||||
|
self.data_files = []
|
||||||
|
|
||||||
|
# Парсинг файлов данных
|
||||||
|
if args.add_data:
|
||||||
|
for data in args.add_data:
|
||||||
|
if ':' in data:
|
||||||
|
src, dst = data.split(':', 1)
|
||||||
|
self.data_files.append((src, dst))
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Неверный формат данных: {data}. Используйте src:dst")
|
||||||
|
|
||||||
|
# Проверка требований
|
||||||
|
if not self.check_requirements():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Настройка окружения
|
||||||
|
if not self.setup_environment():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Генерация информации о версии
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
self.generate_version_info()
|
||||||
|
|
||||||
|
# Сборка (используем упрощенный метод)
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("Выберите метод сборки:")
|
||||||
|
print("1. Упрощенный (рекомендуется)")
|
||||||
|
print("2. С перехватом вывода (может быть проблема с кодировкой)")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
choice = input("Ваш выбор (1 или 2): ").strip()
|
||||||
|
|
||||||
|
success = False
|
||||||
|
if choice == '1':
|
||||||
|
success = self.build_with_pyinstaller_simple()
|
||||||
|
elif choice == '2':
|
||||||
|
success = self.build_with_pyinstaller(clean_build=args.clean)
|
||||||
|
else:
|
||||||
|
print("Используется метод по умолчанию (упрощенный)")
|
||||||
|
success = self.build_with_pyinstaller_simple()
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
print("\n❌ Сборка не удалась.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Оптимизация UPX
|
||||||
|
if args.upx:
|
||||||
|
self.optimize_with_upx()
|
||||||
|
|
||||||
|
# Тестирование
|
||||||
|
self.test_executable()
|
||||||
|
|
||||||
|
# Очистка
|
||||||
|
if args.cleanup:
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("🎉 СБОРКА УСПЕШНО ЗАВЕРШЕНА!")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"\n📦 Ваше приложение готово:")
|
||||||
|
print(f" Файл: dist/{self.app_name}.exe")
|
||||||
|
print(f" Архитектура: 32-битная")
|
||||||
|
print(f" Тип: Один исполняемый файл")
|
||||||
|
print(f" Иконка: {'Да' if self.icon_file else 'Нет'}")
|
||||||
|
|
||||||
|
print("\n📋 Следующие шаги:")
|
||||||
|
print("1. Протестируйте приложение на целевой системе")
|
||||||
|
print("2. Используйте Inno Setup для создания установщика")
|
||||||
|
print("3. Подпишите код для Windows (опционально)")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""Настраивает логирование в файл сессии программы."""
|
||||||
|
session_time = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
|
log_filename = f"session_{session_time}.log"
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(log_filename, encoding='utf-8'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Точка входа"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Сборка 32-битного приложения в один файл',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Примеры использования:
|
||||||
|
python build.py
|
||||||
|
python build.py --name "MyApp" --console
|
||||||
|
python build.py --add-data "data:data" --add-data "config.ini:."
|
||||||
|
python build.py --version-major 2 --version-minor 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Основные параметры
|
||||||
|
parser.add_argument('--name', help='Имя приложения (по умолчанию: MyApp)')
|
||||||
|
parser.add_argument('--main', help='Основной файл Python (по умолчанию: main.py)')
|
||||||
|
|
||||||
|
# Версия
|
||||||
|
parser.add_argument('--version-major', type=int, help='Основная версия')
|
||||||
|
parser.add_argument('--version-minor', type=int, help='Дополнительная версия')
|
||||||
|
parser.add_argument('--version-patch', type=int, help='Патч версия')
|
||||||
|
parser.add_argument('--version-build', type=int, help='Номер сборки')
|
||||||
|
|
||||||
|
# Информация о приложении
|
||||||
|
parser.add_argument('--company', help='Название компании')
|
||||||
|
parser.add_argument('--product', help='Название продукта')
|
||||||
|
parser.add_argument('--description', help='Описание файла')
|
||||||
|
parser.add_argument('--copyright', help='Текст авторских прав')
|
||||||
|
|
||||||
|
# Опции сборки
|
||||||
|
parser.add_argument('--console', action='store_true',
|
||||||
|
help='Показать консоль')
|
||||||
|
parser.add_argument('--no-upx', dest='upx', action='store_false', default=True,
|
||||||
|
help='Не использовать UPX для сжатия')
|
||||||
|
parser.add_argument('--no-clean', dest='clean', action='store_false', default=True,
|
||||||
|
help='Не очищать папку build перед сборкой')
|
||||||
|
|
||||||
|
# Данные и импорты
|
||||||
|
parser.add_argument('--add-data', action='append',
|
||||||
|
help='Добавить файлы данных (формат: src:dst)')
|
||||||
|
parser.add_argument('--hidden-import', action='append',
|
||||||
|
help='Добавить скрытый импорт модуля')
|
||||||
|
|
||||||
|
# Тестирование и очистка
|
||||||
|
parser.add_argument('--test', action='store_true',
|
||||||
|
help='Автоматически протестировать после сборки')
|
||||||
|
parser.add_argument('--no-cleanup', dest='cleanup', action='store_false', default=True,
|
||||||
|
help='Не удалять временные файлы')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Создание и запуск сборщика
|
||||||
|
builder = OneFileBuilder()
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = builder.build(args)
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n⚠️ Сборка прервана пользователем")
|
||||||
|
sys.exit(130)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Неожиданная ошибка: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Вызов настройки логирования при запуске программы
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -360,21 +360,86 @@ class BrowserService:
|
|||||||
def close_current_tab(self):
|
def close_current_tab(self):
|
||||||
"""Закрывает текущую вкладку и переходит на предыдущую."""
|
"""Закрывает текущую вкладку и переходит на предыдущую."""
|
||||||
try:
|
try:
|
||||||
if len(self.driver.window_handles) > 1:
|
logging.info("📑 Метод close_current_tab вызван, но вкладки не будут закрываться.")
|
||||||
logging.info("📑 Закрываю текущую вкладку...")
|
# Удаляем логику закрытия вкладок
|
||||||
self.driver.close()
|
return True
|
||||||
# Переходим на последнюю оставшуюся вкладку
|
|
||||||
self.driver.switch_to.window(self.driver.window_handles[-1])
|
|
||||||
time.sleep(1)
|
|
||||||
logging.info("✅ Перешел на предыдущую вкладку.")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logging.warning("⚠️ Это последняя вкладка, не закрываю.")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"❌ Ошибка при закрытии вкладки: {e}")
|
logging.error(f"❌ Ошибка при попытке закрыть вкладку: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_tabs_count(self):
|
def get_tabs_count(self):
|
||||||
"""Возвращает количество открытых вкладок."""
|
"""Возвращает количество открытых вкладок."""
|
||||||
return len(self.driver.window_handles)
|
return len(self.driver.window_handles)
|
||||||
|
|
||||||
|
def select_tab_by_name(self, tab_name):
|
||||||
|
"""Выбирает вкладку на странице по её названию."""
|
||||||
|
try:
|
||||||
|
logging.info(f"🔍 Поиск вкладки с названием: {tab_name}")
|
||||||
|
# Ищем элемент вкладки по тексту
|
||||||
|
tab_element = self.wait.until(
|
||||||
|
EC.presence_of_element_located((By.XPATH, f"//a[text()='{tab_name}']"))
|
||||||
|
)
|
||||||
|
tab_element.click()
|
||||||
|
logging.info(f"✅ Вкладка '{tab_name}' выбрана.")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"❌ Ошибка при выборе вкладки '{tab_name}': {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def select_tab_by_text_or_name(self, target_text, timeout=30):
|
||||||
|
"""Ищет и выбирает вкладку по тексту или названию, делая повторные попытки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_text (str): Текст или название вкладки для поиска.
|
||||||
|
timeout (int): Общее время ожидания в секундах.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, если вкладка найдена и выбрана, иначе False.
|
||||||
|
"""
|
||||||
|
end_time = time.time() + timeout
|
||||||
|
while time.time() < end_time:
|
||||||
|
try:
|
||||||
|
logging.info(f"🔍 Поиск вкладки с текстом или названием: {target_text}")
|
||||||
|
# Ищем элемент вкладки по тексту
|
||||||
|
tab_element = self.wait.until(
|
||||||
|
EC.presence_of_element_located((By.XPATH, f"//*[text()='{target_text}']"))
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView(true);", tab_element)
|
||||||
|
time.sleep(0.5)
|
||||||
|
tab_element.click()
|
||||||
|
logging.info(f"✅ Вкладка с текстом '{target_text}' выбрана.")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"⚠️ Вкладка с текстом '{target_text}' не найдена, повторная попытка...")
|
||||||
|
time.sleep(1) # Ждем перед повторной попыткой
|
||||||
|
logging.error(f"❌ Не удалось найти вкладку с текстом '{target_text}' за {timeout} секунд.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_tabs(self, tabs):
|
||||||
|
"""Устанавливает массив с названиями вкладок."""
|
||||||
|
self.tabs = tabs
|
||||||
|
|
||||||
|
def select_tab_from_list(self, tab_index):
|
||||||
|
"""Выбирает вкладку из массива по индексу.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab_index (int): Индекс вкладки в массиве.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, если вкладка выбрана успешно, иначе False.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not hasattr(self, 'tabs') or not self.tabs:
|
||||||
|
logging.error("❌ Массив вкладок не установлен или пуст.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if tab_index < 0 or tab_index >= len(self.tabs):
|
||||||
|
logging.error("❌ Индекс вкладки выходит за пределы массива.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
tab_name = self.tabs[tab_index]
|
||||||
|
logging.info(f"🔍 Выбор вкладки с индексом {tab_index}: {tab_name}")
|
||||||
|
return self.select_tab_by_name(tab_name)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"❌ Ошибка при выборе вкладки по индексу {tab_index}: {e}")
|
||||||
|
return False
|
||||||
|
|||||||
0
test1.xlsx
Normal file
0
test1.xlsx
Normal file
0
test2.xlsx
Normal file
0
test2.xlsx
Normal file
0
test3.xlsx
Normal file
0
test3.xlsx
Normal file
0
test5.xlsx
Normal file
0
test5.xlsx
Normal file
0
tset4.xlsx
Normal file
0
tset4.xlsx
Normal file
92
ui.py
92
ui.py
@@ -7,6 +7,7 @@ import threading
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from services.processor import FileProcessorService
|
from services.processor import FileProcessorService
|
||||||
|
from services.browser_service import BrowserService
|
||||||
|
|
||||||
|
|
||||||
class UIManager:
|
class UIManager:
|
||||||
@@ -16,7 +17,7 @@ class UIManager:
|
|||||||
"""Инициализирует интерфейс."""
|
"""Инициализирует интерфейс."""
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("🔄 Автоматизация загрузки файлов")
|
self.root.title("🔄 Автоматизация загрузки файлов")
|
||||||
self.root.geometry("700x600")
|
self.root.geometry("700x800")
|
||||||
self.root.configure(bg="#2C3E50")
|
self.root.configure(bg="#2C3E50")
|
||||||
self.root.minsize(500, 400)
|
self.root.minsize(500, 400)
|
||||||
|
|
||||||
@@ -92,9 +93,23 @@ class UIManager:
|
|||||||
)
|
)
|
||||||
url_label.pack(anchor="w", pady=(10, 5))
|
url_label.pack(anchor="w", pady=(10, 5))
|
||||||
|
|
||||||
self.url_entry = tk.Entry(main_frame, font=self.font_label)
|
# Кнопка Вставить рядом с полем URL
|
||||||
self.url_entry.pack(anchor="w", fill=tk.X, pady=(0, 15))
|
url_frame = tk.Frame(main_frame, bg="#2C3E50")
|
||||||
self.url_entry.insert(0, "https://")
|
url_frame.pack(fill=tk.X, pady=(0, 15))
|
||||||
|
|
||||||
|
self.url_entry = tk.Entry(url_frame, font=self.font_label)
|
||||||
|
self.url_entry.pack(side="left", fill=tk.X, expand=True)
|
||||||
|
|
||||||
|
insert_button = tk.Button(
|
||||||
|
url_frame,
|
||||||
|
text="Вставить",
|
||||||
|
command=self._insert_from_clipboard,
|
||||||
|
font=self.font_button,
|
||||||
|
bg="#27AE60",
|
||||||
|
fg="white",
|
||||||
|
padx=10
|
||||||
|
)
|
||||||
|
insert_button.pack(side="left", padx=(10, 0))
|
||||||
|
|
||||||
# ===== ПОЛЕ 3: ВАРИАНТЫ КНОПОК =====
|
# ===== ПОЛЕ 3: ВАРИАНТЫ КНОПОК =====
|
||||||
buttons_label = tk.Label(
|
buttons_label = tk.Label(
|
||||||
@@ -190,6 +205,19 @@ class UIManager:
|
|||||||
|
|
||||||
# Перенаправление логов в текстовое поле
|
# Перенаправление логов в текстовое поле
|
||||||
self._setup_log_handler()
|
self._setup_log_handler()
|
||||||
|
|
||||||
|
# ===== ВЫБОР ВКЛАДКИ =====
|
||||||
|
tabs_label = tk.Label(
|
||||||
|
main_frame,
|
||||||
|
text="📑 Выбор вкладки браузера:",
|
||||||
|
font=self.font_label,
|
||||||
|
bg="#2C3E50",
|
||||||
|
fg="#ECF0F1"
|
||||||
|
)
|
||||||
|
tabs_label.pack(anchor="w", pady=(10, 5))
|
||||||
|
|
||||||
|
self.tab_selector = TabSelectorApp(main_frame, BrowserService(download_dir="./downloads"))
|
||||||
|
self.tab_selector.pack(fill=tk.X, pady=(0, 15))
|
||||||
|
|
||||||
def _setup_log_handler(self):
|
def _setup_log_handler(self):
|
||||||
"""Настраивает обработчик логов в текстовое поле."""
|
"""Настраивает обработчик логов в текстовое поле."""
|
||||||
@@ -273,6 +301,15 @@ class UIManager:
|
|||||||
# Создаем процессор
|
# Создаем процессор
|
||||||
processor = FileProcessorService(directory, url, download_dir)
|
processor = FileProcessorService(directory, url, download_dir)
|
||||||
|
|
||||||
|
# Проверка вкладок перед началом загрузки
|
||||||
|
browser_service = BrowserService(download_dir="./downloads")
|
||||||
|
browser_service.set_tabs(["Вкладка 1", "Вкладка 2", "Вкладка 3"])
|
||||||
|
|
||||||
|
# Пример: выбор вкладки по индексу
|
||||||
|
if not browser_service.select_tab_from_list(0):
|
||||||
|
logging.error("❌ Не удалось выбрать вкладку. Проверьте массив вкладок.")
|
||||||
|
return
|
||||||
|
|
||||||
# Запускаем обработку
|
# Запускаем обработку
|
||||||
results = processor.start_processing(upload_texts, process_texts)
|
results = processor.start_processing(upload_texts, process_texts)
|
||||||
|
|
||||||
@@ -310,6 +347,53 @@ class UIManager:
|
|||||||
|
|
||||||
messagebox.showinfo("Результаты", result_text)
|
messagebox.showinfo("Результаты", result_text)
|
||||||
|
|
||||||
|
def _insert_from_clipboard(self):
|
||||||
|
"""Обработчик для вставки текста из буфера обмена."""
|
||||||
|
try:
|
||||||
|
clipboard_text = self.root.clipboard_get()
|
||||||
|
self.url_entry.delete(0, tk.END) # Очистить поле
|
||||||
|
self.url_entry.insert(0, clipboard_text) # Вставить текст из буфера
|
||||||
|
except tk.TclError:
|
||||||
|
messagebox.showerror("Ошибка", "Буфер обмена пуст или недоступен.")
|
||||||
|
|
||||||
|
|
||||||
|
class TabSelectorApp:
|
||||||
|
def __init__(self, parent, browser_service):
|
||||||
|
self.browser_service = browser_service
|
||||||
|
self.frame = tk.Frame(parent, bg="#2C3E50")
|
||||||
|
|
||||||
|
# Выпадающий список
|
||||||
|
self.combo = ttk.Combobox(self.frame, state="readonly")
|
||||||
|
self.combo.pack(pady=10)
|
||||||
|
|
||||||
|
# Кнопка для выбора вкладки
|
||||||
|
self.select_button = tk.Button(self.frame, text="Выбрать вкладку", command=self.select_tab)
|
||||||
|
self.select_button.pack(pady=10)
|
||||||
|
|
||||||
|
def pack(self, **kwargs):
|
||||||
|
"""Упрощает упаковку фрейма."""
|
||||||
|
self.frame.pack(**kwargs)
|
||||||
|
|
||||||
|
def set_tabs(self, tabs):
|
||||||
|
"""Устанавливает массив вкладок и обновляет выпадающий список."""
|
||||||
|
self.tabs = tabs
|
||||||
|
self.combo["values"] = self.tabs
|
||||||
|
if self.tabs:
|
||||||
|
self.combo.current(0) # Устанавливаем первый элемент по умолчанию
|
||||||
|
|
||||||
|
def select_tab(self):
|
||||||
|
"""Выбирает вкладку из выпадающего списка."""
|
||||||
|
selected_index = self.combo.current()
|
||||||
|
if selected_index == -1:
|
||||||
|
print("❌ Вкладка не выбрана.")
|
||||||
|
return
|
||||||
|
|
||||||
|
success = self.browser_service.select_tab_from_list(selected_index)
|
||||||
|
if success:
|
||||||
|
print(f"✅ Вкладка '{self.tabs[selected_index]}' выбрана.")
|
||||||
|
else:
|
||||||
|
print(f"❌ Не удалось выбрать вкладку '{self.tabs[selected_index]}'." )
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Главная функция."""
|
"""Главная функция."""
|
||||||
|
|||||||
Reference in New Issue
Block a user