Files
kadastr-17/build_32bit.py
2026-03-12 01:50:57 +05:00

270 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Скрипт сборки Программы 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)