Files
test-loderweb/build.py
unknown be7aadf218 new
2026-02-09 14:28:39 +05:00

589 lines
24 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()