589 lines
24 KiB
Python
589 lines
24 KiB
Python
#!/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() |