270 lines
11 KiB
Python
270 lines
11 KiB
Python
"""
|
||
Скрипт сборки Программы 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)
|