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)