This commit is contained in:
unknown
2026-02-09 14:28:39 +05:00
parent 5fd5de3baf
commit be7aadf218
10 changed files with 754 additions and 16 deletions

589
build.py Normal file
View 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()