- build Inno installer without SetupIconFile to avoid code 2 failures in runner - drop MyIconFile define and pass only essential defines - make task description ASCII-only to avoid encoding issues
255 lines
9.1 KiB
Python
255 lines
9.1 KiB
Python
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from app_version import APP_VERSION
|
||
|
||
# --- Конфигурация ---
|
||
APP_NAME = "AnabasisManager"
|
||
UPDATER_NAME = "AnabasisUpdater"
|
||
VERSION = APP_VERSION # Единая версия приложения
|
||
MAIN_SCRIPT = "main.py"
|
||
UPDATER_SCRIPT = "updater_gui.py"
|
||
ICON_PATH = "icon.ico"
|
||
INSTALLER_SCRIPT = os.path.join("installer", "AnabasisManager.iss")
|
||
DIST_DIR = os.path.join("dist", APP_NAME)
|
||
ARCHIVE_NAME = f"{APP_NAME}-{VERSION}" # Формат Название-Версия
|
||
INSTALLER_NAME = f"{APP_NAME}-setup-{VERSION}.exe"
|
||
SAFE_CLEAN_ROOT_FILES = {"main.py", "updater_gui.py", "requirements.txt", "build.py"}
|
||
REMOVE_LIST = [
|
||
"Qt6Pdf.dll", "Qt6PdfQuick.dll", "Qt6PdfWidgets.dll",
|
||
"Qt6VirtualKeyboard.dll", "Qt6Positioning.dll",
|
||
"Qt6PrintSupport.dll", "Qt6Svg.dll", "Qt6Sql.dll",
|
||
"Qt6Charts.dll", "Qt6Multimedia.dll", "Qt63DCore.dll",
|
||
"translations",
|
||
"Qt6QuickTemplates2.dll"
|
||
]
|
||
|
||
|
||
def write_version_marker():
|
||
marker_path = os.path.join(DIST_DIR, "version.txt")
|
||
try:
|
||
os.makedirs(DIST_DIR, exist_ok=True)
|
||
with open(marker_path, "w", encoding="utf-8") as f:
|
||
f.write(str(VERSION).strip() + "\n")
|
||
print(f"[OK] Обновлен маркер версии: {marker_path}")
|
||
except Exception as e:
|
||
print(f"[ERROR] Не удалось записать version.txt: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def copy_icon_to_dist():
|
||
icon_abs_path = os.path.abspath(ICON_PATH)
|
||
if not os.path.exists(icon_abs_path):
|
||
print("[WARN] icon.ico не найден, пропуск копирования иконки в dist.")
|
||
return
|
||
try:
|
||
os.makedirs("dist", exist_ok=True)
|
||
os.makedirs(DIST_DIR, exist_ok=True)
|
||
shutil.copy2(icon_abs_path, os.path.join("dist", "icon.ico"))
|
||
shutil.copy2(icon_abs_path, os.path.join(DIST_DIR, "icon.ico"))
|
||
print("[OK] Иконка скопирована в dist/icon.ico и dist/AnabasisManager/icon.ico")
|
||
except Exception as e:
|
||
print(f"[ERROR] Не удалось скопировать icon.ico в dist: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def ensure_project_root():
|
||
missing = [name for name in SAFE_CLEAN_ROOT_FILES if not os.path.exists(name)]
|
||
if missing:
|
||
print("[ERROR] Скрипт нужно запускать из корня проекта.")
|
||
print(f"[ERROR] Не найдены: {', '.join(missing)}")
|
||
sys.exit(1)
|
||
|
||
|
||
def run_build():
|
||
print(f"--- 1. Запуск PyInstaller для {APP_NAME} v{VERSION} ---")
|
||
icon_abs_path = os.path.abspath(ICON_PATH)
|
||
has_icon = os.path.exists(icon_abs_path)
|
||
|
||
command = [
|
||
"pyinstaller",
|
||
"--noconfirm",
|
||
"--onedir",
|
||
"--windowed",
|
||
"--exclude-module", "PySide6.QtWebEngineCore",
|
||
"--exclude-module", "PySide6.QtWebEngineWidgets",
|
||
"--exclude-module", "PySide6.QtWebEngineQuick",
|
||
f"--name={APP_NAME}",
|
||
f"--icon={icon_abs_path}" if has_icon else "",
|
||
f"--add-data={icon_abs_path}{os.pathsep}." if has_icon else "",
|
||
f"--add-data=auth_webview.py{os.pathsep}.",
|
||
MAIN_SCRIPT
|
||
]
|
||
|
||
command = [arg for arg in command if arg]
|
||
|
||
try:
|
||
subprocess.check_call(command)
|
||
print("\n[OK] Сборка PyInstaller завершена.")
|
||
except subprocess.CalledProcessError as e:
|
||
print(f"\n[ERROR] Ошибка при сборке: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def run_updater_build():
|
||
print(f"\n--- 1.2 Сборка {UPDATER_NAME} ---")
|
||
icon_abs_path = os.path.abspath(ICON_PATH)
|
||
has_icon = os.path.exists(icon_abs_path)
|
||
updater_spec_dir = os.path.join("build", "updater_spec")
|
||
updater_spec_path = os.path.join(updater_spec_dir, f"{UPDATER_NAME}.spec")
|
||
if os.path.exists(updater_spec_path):
|
||
os.remove(updater_spec_path)
|
||
command = [
|
||
"pyinstaller",
|
||
"--noconfirm",
|
||
"--clean",
|
||
"--onefile",
|
||
"--windowed",
|
||
f"--name={UPDATER_NAME}",
|
||
"--distpath", DIST_DIR,
|
||
"--workpath", os.path.join("build", "updater"),
|
||
"--specpath", updater_spec_dir,
|
||
f"--icon={icon_abs_path}" if has_icon else "",
|
||
UPDATER_SCRIPT,
|
||
]
|
||
command = [arg for arg in command if arg]
|
||
try:
|
||
subprocess.check_call(command)
|
||
print(f"[OK] {UPDATER_NAME} собран.")
|
||
except subprocess.CalledProcessError as e:
|
||
print(f"[ERROR] Ошибка при сборке {UPDATER_NAME}: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def run_cleanup():
|
||
print(f"\n--- 2. Оптимизация папки {APP_NAME} ---")
|
||
|
||
# Пытаемся найти папку PySide6 внутри сборки
|
||
pyside_path = os.path.join(DIST_DIR, "PySide6")
|
||
if not os.path.exists(pyside_path):
|
||
pyside_path = DIST_DIR
|
||
|
||
for item in REMOVE_LIST:
|
||
path = os.path.join(pyside_path, item)
|
||
if os.path.exists(path):
|
||
try:
|
||
if os.path.isdir(path):
|
||
shutil.rmtree(path)
|
||
else:
|
||
os.remove(path)
|
||
print(f"Удалено: {item}")
|
||
except Exception as e:
|
||
print(f"Пропуск {item}: {e}")
|
||
|
||
|
||
def create_archive():
|
||
print(f"\n--- 3. Создание архива {ARCHIVE_NAME}.zip ---")
|
||
|
||
try:
|
||
# Создаем zip-архив из папки DIST_DIR
|
||
# base_name - имя файла без расширения, format - 'zip', root_dir - что упаковываем
|
||
shutil.make_archive(os.path.join("dist", ARCHIVE_NAME), 'zip', DIST_DIR)
|
||
print(f"[OK] Архив создан: dist/{ARCHIVE_NAME}.zip")
|
||
except Exception as e:
|
||
print(f"[ERROR] Не удалось создать архив: {e}")
|
||
|
||
|
||
def _find_iscc():
|
||
candidates = []
|
||
iscc_env = os.getenv("ISCC_PATH", "").strip()
|
||
if iscc_env:
|
||
candidates.append(iscc_env)
|
||
candidates.append(shutil.which("iscc"))
|
||
candidates.append(shutil.which("ISCC.exe"))
|
||
candidates.append(r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe")
|
||
candidates.append(r"C:\Program Files\Inno Setup 6\ISCC.exe")
|
||
for candidate in candidates:
|
||
if candidate and os.path.exists(candidate):
|
||
return candidate
|
||
return ""
|
||
|
||
|
||
def _decode_process_output(raw_bytes):
|
||
if raw_bytes is None:
|
||
return ""
|
||
if isinstance(raw_bytes, str):
|
||
return raw_bytes
|
||
for enc in ("utf-8", "cp1251", "cp866"):
|
||
try:
|
||
return raw_bytes.decode(enc)
|
||
except Exception:
|
||
continue
|
||
return raw_bytes.decode("utf-8", errors="replace")
|
||
|
||
|
||
def build_installer():
|
||
print(f"\n--- 4. Создание установщика {INSTALLER_NAME} ---")
|
||
if os.name != "nt":
|
||
print("[INFO] Установщик Inno Setup создается только на Windows. Шаг пропущен.")
|
||
return
|
||
if not os.path.exists(INSTALLER_SCRIPT):
|
||
print(f"[ERROR] Не найден скрипт установщика: {INSTALLER_SCRIPT}")
|
||
sys.exit(1)
|
||
if not os.path.exists(DIST_DIR):
|
||
print(f"[ERROR] Не найдена папка сборки приложения: {DIST_DIR}")
|
||
sys.exit(1)
|
||
iscc_path = _find_iscc()
|
||
if not iscc_path:
|
||
print("[ERROR] Не найден Inno Setup Compiler (ISCC.exe).")
|
||
print("[ERROR] Установите Inno Setup 6 или задайте переменную окружения ISCC_PATH.")
|
||
sys.exit(1)
|
||
|
||
icon_abs_path = os.path.abspath(ICON_PATH)
|
||
command = [
|
||
iscc_path,
|
||
f"/DMyAppVersion={VERSION}",
|
||
f"/DMySourceDir={os.path.abspath(DIST_DIR)}",
|
||
f"/DMyOutputDir={os.path.abspath('dist')}",
|
||
os.path.abspath(INSTALLER_SCRIPT),
|
||
]
|
||
try:
|
||
completed = subprocess.run(
|
||
command,
|
||
capture_output=True,
|
||
check=False,
|
||
)
|
||
stdout_text = _decode_process_output(completed.stdout)
|
||
stderr_text = _decode_process_output(completed.stderr)
|
||
if stdout_text:
|
||
print(stdout_text.rstrip())
|
||
if stderr_text:
|
||
print(stderr_text.rstrip())
|
||
if completed.returncode != 0:
|
||
raise RuntimeError(f"ISCC exited with code {completed.returncode}")
|
||
installer_path = os.path.join("dist", INSTALLER_NAME)
|
||
if not os.path.exists(installer_path):
|
||
print(f"[ERROR] Установщик не создан: {installer_path}")
|
||
sys.exit(1)
|
||
print(f"[OK] Установщик создан: {installer_path}")
|
||
except Exception as e:
|
||
print(f"[ERROR] Ошибка при создании установщика: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
ensure_project_root()
|
||
# Предварительная очистка
|
||
for folder in ["build", "dist"]:
|
||
if os.path.exists(folder):
|
||
shutil.rmtree(folder)
|
||
|
||
run_build()
|
||
run_updater_build()
|
||
run_cleanup()
|
||
copy_icon_to_dist()
|
||
write_version_marker()
|
||
create_archive()
|
||
build_installer()
|
||
|
||
print("\n" + "=" * 30)
|
||
print("ПРОЦЕСС ЗАВЕРШЕН")
|
||
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
|
||
print(f"Установщик: dist/{INSTALLER_NAME}")
|
||
print("=" * 30)
|