""" Скрипт сборки Программы 17 (Kadastr-17) в 32-битный исполняемый файл. Требования перед запуском: • 32-битный Python (3.8 — 3.12) установлен и доступен в PATH или PYTHON32 • PyInstaller, openpyxl, Pillow установлены для этого интерпретатора Быстрый старт на Windows: py -3.8-32 build_32bit.py Быстрый старт на Linux (32-bit Python в /usr/bin/python3.8): PYTHON32=/path/to/python3.8-32bit python3 build_32bit.py Переменная окружения PYTHON32 позволяет указать нужный интерпретатор явно. """ import os import subprocess import sys from pathlib import Path # ─────────────────────────────── Конфигурация ───────────────────────────── # APP_NAME = "KadastrParser17" MAIN_SCRIPT = "main.py" DIST_DIR = "dist_32bit" BUILD_DIR = "build_32bit" # Имя результирующего файла (на Windows добавится .exe автоматически) EXE_NAME = f"{APP_NAME}_32bit" # Разделитель путей в --add-data: ':' на Linux/macOS, ';' на Windows _SEP = ";" if sys.platform == "win32" else ":" # Файлы/папки данных, которые нужно включить в сборку. # Формат: (источник, папка-назначение-внутри-exe) ADD_DATA = [ # Шаблон формы — программа ищет его по пути data/ШАБЛОН ВЫЯВЛЕННЫЕ-1.xlsx (f"data{_SEP}data"), # Пакет services (на случай если PyInstaller его не соберёт автоматически) (f"services{_SEP}services"), # Вспомогательный MIF-парсер, лежащий в корне проекта (f"read_mif_file.py{_SEP}."), ] # Hidden imports — модули, которые PyInstaller не обнаруживает автоматически HIDDEN_IMPORTS = [ "tkinter", "tkinter.ttk", "tkinter.filedialog", "tkinter.messagebox", "openpyxl", "openpyxl.styles", "openpyxl.drawing.image", "openpyxl.drawing.spreadsheet_drawing", "PIL", "PIL.Image", "xml.etree.ElementTree", "zipfile", "pathlib", "re", "io", "copy", "datetime", "threading", ] # ─────────────────────────────── Утилиты ────────────────────────────────── # def find_python32() -> str: """ Найти 32-битный интерпретатор Python. Порядок поиска: 1. Переменная окружения PYTHON32 2. Лаунчер Windows 'py -3.X-32' (проверяем версии 3.12..3.8) 3. Попытка запустить текущий интерпретатор (sys.executable) как fallback """ # 1. Явно указано пользователем env_py = os.environ.get("PYTHON32", "") if env_py: p = Path(env_py) if p.is_file(): print(f" Используем PYTHON32={env_py}") return str(p) else: print(f" PYTHON32={env_py} не найден, ищем автоматически…") # 2. Windows py-лаунчер if sys.platform == "win32": for minor in range(12, 7, -1): tag = f"-3.{minor}-32" try: result = subprocess.run( ["py", tag, "--version"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: print(f" Найден 32-bit Python через лаунчер: py {tag}") return f"py {tag}" # специальный маркер — см. _build_args() except (FileNotFoundError, subprocess.TimeoutExpired): continue # 3. Текущий интерпретатор (может быть 64-bit — предупредим) print(f" Используем текущий интерпретатор: {sys.executable}") _warn_if_64bit(sys.executable) return sys.executable def _warn_if_64bit(python_exe: str): bits = subprocess.run( [python_exe, "-c", "import struct; print(struct.calcsize('P')*8)"], capture_output=True, text=True ).stdout.strip() if bits == "64": print(" ⚠ Внимание: выбранный Python — 64-bit!") print(" Установите 32-bit Python и укажите путь через PYTHON32=…") elif bits == "32": print(" ✓ Python 32-bit подтверждён") def _build_args(python_interp: str) -> list: """Собрать аргументы командной строки для вызова PyInstaller.""" # Если интерпретатор — это 'py X.Y-32' (Windows лаунчер) if python_interp.startswith("py "): parts = python_interp.split() # ['py', '-3.X-32'] prefix = parts # ['py', '-3.X-32', '-m', 'PyInstaller'] prefix += ["-m", "PyInstaller"] else: prefix = [python_interp, "-m", "PyInstaller"] args = prefix + [ MAIN_SCRIPT, "--onefile", "--windowed", f"--name={EXE_NAME}", "--clean", "--noconfirm", f"--distpath={DIST_DIR}", f"--workpath={BUILD_DIR}", "--specpath=.", ] for data_item in ADD_DATA: args += ["--add-data", data_item] for imp in HIDDEN_IMPORTS: args += ["--hidden-import", imp] return args # ─────────────────────────── Основные функции ───────────────────────────── # def build(): print("=" * 55) print(" Сборка Программы 17 — KadastrParser (32-bit)") print("=" * 55) # Проверяем рабочую директорию if not Path(MAIN_SCRIPT).is_file(): print(f"Файл {MAIN_SCRIPT} не найден.") print("Запустите скрипт из корневой папки проекта.") return False # Ищем 32-bit Python print("\n[1/3] Поиск 32-битного интерпретатора Python…") python = find_python32() # Формируем команду print("\n[2/3] Запуск PyInstaller…") cmd = _build_args(python) print(" Команда:", " ".join(cmd)) print() try: subprocess.run(cmd, check=True) except subprocess.CalledProcessError as e: print(f"\nОшибка PyInstaller (код {e.returncode})") return False except FileNotFoundError as e: print(f"\nФайл не найден: {e}") print("Убедитесь, что PyInstaller установлен для выбранного интерпретатора:") print(f" {python} -m pip install pyinstaller") return False # Проверяем результат print("\n[3/3] Проверка результата…") exe_candidates = [ Path(DIST_DIR) / f"{EXE_NAME}.exe", # Windows Path(DIST_DIR) / EXE_NAME, # Linux/macOS ] exe_path = next((p for p in exe_candidates if p.exists()), None) if exe_path is None: print("Исполняемый файл не найден в папке dist_32bit/") return False size_mb = exe_path.stat().st_size / (1024 * 1024) print(f" Файл: {exe_path}") print(f" Размер: {size_mb:.1f} MB") _check_architecture(exe_path) return True def _check_architecture(exe_path: Path): """Определить разрядность .exe по первым байтам PE-заголовка.""" try: with open(exe_path, "rb") as f: dos_header = f.read(64) if dos_header[:2] != b"MZ": return pe_offset = int.from_bytes(dos_header[60:64], "little") f.seek(pe_offset) pe_sig = f.read(4) if pe_sig != b"PE\x00\x00": return machine = int.from_bytes(f.read(2), "little") MACHINE = {0x014C: "32-bit (x86)", 0x8664: "64-bit (x86-64)"} arch = MACHINE.get(machine, f"0x{machine:04X}") mark = "✓" if "32" in arch else "⚠" print(f" Архитектура: {arch} {mark}") except Exception: pass # Не критично — просто пропускаем def create_readme(): dist = Path(DIST_DIR) dist.mkdir(exist_ok=True) readme = dist / "README.txt" readme.write_text( "Программа 17 — Заполнение формы по выявленным-1\n" "================================================\n\n" "Системные требования\n" "--------------------\n" " Windows 7 / 8 / 10 / 11 (32-bit или 64-bit)\n" " Минимум 256 MB оперативной памяти\n\n" "Запуск\n" "------\n" " Дважды щёлкните KadastrParser17_32bit.exe\n\n" "Исходные данные\n" "---------------\n" " Таблица 1 — сведения о кадастровом инженере (.xlsx)\n" " Таблица 2 — перечень выявленных (.xlsx)\n" " Таблица 3 — схемы (.xlsx с PNG, опционально)\n" " Папка MIF — MIF/MID файлы с координатами\n" " Шаблон — ШАБЛОН ВЫЯВЛЕННЫЕ-1.xlsx\n\n" "Результаты\n" "----------\n" " output/ — заполненные формы + ВСЕ_ФОРМЫ.xlsx + Таблица4…xlsx\n", encoding="utf-8", ) print(f" README создан: {readme}") # ─────────────────────────────── Точка входа ────────────────────────────── # if __name__ == "__main__": ok = build() if ok: create_readme() print("\nСборка завершена успешно!") print(f"Файлы находятся в папке '{DIST_DIR}/'") else: print("\nСборка завершилась с ошибкой.") sys.exit(1)