#!/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()