This commit is contained in:
2026-03-12 01:50:57 +05:00
parent ec82b528a3
commit c33befe7c2
12 changed files with 1553 additions and 69 deletions

269
build_32bit.py Normal file
View File

@@ -0,0 +1,269 @@
"""
Скрипт сборки Программы 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)