- use static relative paths in .iss for source/output - pass only version and /O override to ISCC - add explicit source/script path diagnostics in build.py
267 lines
9.0 KiB
Python
267 lines
9.0 KiB
Python
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from app_version import APP_VERSION
|
|
|
|
# --- Configuration ---
|
|
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] Version marker written: {marker_path}")
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to write 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 not found, skipping icon copy into 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] Icon copied to dist/icon.ico and dist/AnabasisManager/icon.ico")
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to copy icon.ico into 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] Run this script from the project root.")
|
|
print(f"[ERROR] Missing files: {', '.join(missing)}")
|
|
sys.exit(1)
|
|
|
|
|
|
def run_build():
|
|
print(f"--- 1. Running PyInstaller for {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 build completed.")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"\n[ERROR] Build failed: {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
def run_updater_build():
|
|
print(f"\n--- 1.2 Building {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} built.")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[ERROR] Failed to build {UPDATER_NAME}: {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
def run_cleanup():
|
|
print(f"\n--- 2. Optimizing {APP_NAME} folder ---")
|
|
|
|
# Пытаемся найти папку 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"Removed: {item}")
|
|
except Exception as e:
|
|
print(f"Skipped {item}: {e}")
|
|
|
|
|
|
def create_archive():
|
|
print(f"\n--- 3. Creating archive {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] Archive created: dist/{ARCHIVE_NAME}.zip")
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to create archive: {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-sig", "utf-16", "utf-16-le", "cp1251", "cp866", "latin-1"):
|
|
try:
|
|
return raw_bytes.decode(enc)
|
|
except Exception:
|
|
continue
|
|
return raw_bytes.decode("utf-8", errors="replace")
|
|
|
|
|
|
def build_installer():
|
|
print(f"\n--- 4. Building installer {INSTALLER_NAME} ---")
|
|
if os.name != "nt":
|
|
print("[INFO] Inno Setup installer is built only on Windows. Step skipped.")
|
|
return
|
|
if not os.path.exists(INSTALLER_SCRIPT):
|
|
print(f"[ERROR] Installer script not found: {INSTALLER_SCRIPT}")
|
|
sys.exit(1)
|
|
if not os.path.exists(DIST_DIR):
|
|
print(f"[ERROR] Build output folder not found: {DIST_DIR}")
|
|
sys.exit(1)
|
|
iscc_path = _find_iscc()
|
|
if not iscc_path:
|
|
print("[ERROR] Inno Setup Compiler (ISCC.exe) not found.")
|
|
print("[ERROR] Install Inno Setup 6 or set ISCC_PATH environment variable.")
|
|
sys.exit(1)
|
|
|
|
project_root = os.path.abspath(".")
|
|
source_dir = os.path.abspath(DIST_DIR)
|
|
output_dir = os.path.abspath("dist")
|
|
iss_path = os.path.abspath(INSTALLER_SCRIPT)
|
|
print(f"[INFO] ISCC source dir: {source_dir}")
|
|
print(f"[INFO] ISCC output dir: {output_dir}")
|
|
print(f"[INFO] ISCC script: {iss_path}")
|
|
if not os.path.exists(source_dir):
|
|
print(f"[ERROR] Source dir does not exist: {source_dir}")
|
|
sys.exit(1)
|
|
if not os.path.exists(iss_path):
|
|
print(f"[ERROR] Installer script does not exist: {iss_path}")
|
|
sys.exit(1)
|
|
command = [
|
|
iscc_path,
|
|
f"/DMyAppVersion={VERSION}",
|
|
f"/O{output_dir}",
|
|
iss_path,
|
|
]
|
|
try:
|
|
completed = subprocess.run(
|
|
command,
|
|
capture_output=True,
|
|
cwd=project_root,
|
|
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 was not created: {installer_path}")
|
|
sys.exit(1)
|
|
print(f"[OK] Installer created: {installer_path}")
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to build installer: {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("BUILD COMPLETED")
|
|
print(f"Release archive: dist/{ARCHIVE_NAME}.zip")
|
|
print(f"Installer: dist/{INSTALLER_NAME}")
|
|
print("=" * 30)
|