Files
fgl-aircon/pyfglair/_corebuild.py
T
petr.polezhaev 0292404756 ha(H1): pyfglair — cffi-ядро (очередь событий), wheel, CLI discover/monitor
- CMake: shared-таргет fgl-aircon-shared (FGL_BUILD_SHARED, PIC, SOVERSION)
- C-API: fgl_prop_raw_range + кольцевые очереди событий/логов
  (fgl_session_poll/wait_events, fgl_log_poll/wait_events): прямые колбэки
  из потоков ядра со стеком 8 КБ небезопасны для интерпретатора Python
- pyfglair: _corebuild (cmake->wheel), _cffi (ABI), Session (reader-поток ->
  loop.call_soon_threadsafe), templates (интроспекция C-таблиц), provision
  (aiohttp), CLI discover/monitor + esphome-secrets
- setup.py/pyproject: платформенный wheel py3-none-linux_x86_64 с .so
- tests/pyfglair: templates, provision (мок-облако), session + CLI против
  mock_ac.py; scripts/py-ci.sh; ci.sh запускает python-часть
2026-09-29 11:31:57 +03:00

136 lines
4.3 KiB
Python

"""Сборка libfgl-aircon.so из корня монорепо (cmake).
Используется при упаковке wheel (setup.py) и для локальной разработки:
python -m pyfglair._corebuild --inplace
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
LIB_GLOBS = (
"libfgl-aircon.so",
"libfgl-aircon.so.*",
"libfgl-aircon.*.dylib",
"libfgl-aircon.dylib",
"fgl-aircon.dll",
)
def source_root() -> Path | None:
"""Корень монорепо (CMakeLists.txt + include/fgl-aircon)."""
env = os.environ.get("FGLAIR_SOURCE_ROOT")
if env:
root = Path(env).resolve()
if (root / "CMakeLists.txt").exists():
return root
for parent in Path(__file__).resolve().parents:
if (parent / "include" / "fgl-aircon").is_dir() and (
parent / "CMakeLists.txt"
).exists():
return parent
return None
def _run(argv: list[str], cwd: Path | None = None) -> None:
print("+", " ".join(argv), flush=True)
subprocess.run(argv, cwd=cwd, check=True)
def find_built_library(build_dir: Path) -> Path:
found: list[Path] = []
for pattern in LIB_GLOBS:
found.extend(build_dir.rglob(pattern))
real = {p.resolve() for p in found if p.resolve().is_file()}
if not real:
raise FileNotFoundError(
f"libfgl-aircon не найдена после сборки в {build_dir}"
)
return max(real, key=lambda p: p.stat().st_mtime)
def build_core(
dest: Path | None = None,
*,
source: Path | None = None,
build_dir: Path | None = None,
jobs: int | None = None,
clean: bool = False,
) -> Path:
"""Собирает shared-ядро и (при dest) копирует его в каталог пакета.
Возвращает путь к скопированной/собранной библиотеке.
"""
src = Path(source) if source else source_root()
if src is None:
raise RuntimeError(
"не найден корень fgl-aircon (CMakeLists.txt); задайте "
"FGLAIR_SOURCE_ROOT"
)
src = Path(src).resolve()
build = Path(build_dir) if build_dir else src / "build-pyfglair"
if clean and build.exists():
shutil.rmtree(build)
generator = ["-G", "Ninja"] if shutil.which("ninja") else []
_run([
"cmake", "-S", str(src), "-B", str(build), *generator,
"-DCMAKE_BUILD_TYPE=Release",
"-DFGL_BUILD_TESTS=OFF",
"-DFGL_BUILD_EXAMPLES=OFF",
"-DFGL_BUILD_SHARED=ON",
])
_run([
"cmake", "--build", str(build), "--target", "fgl-aircon-shared",
"--parallel", str(jobs or os.cpu_count() or 2),
])
lib = find_built_library(build)
if dest is None:
return lib
dest = Path(dest)
dest.mkdir(parents=True, exist_ok=True)
target = dest / lib.name
shutil.copy2(lib, target)
return target
def ensure_core(dest: Path | None = None) -> Path:
"""Собирает ядро, если рядом с пакетом нет готовой библиотеки."""
pkg = Path(__file__).resolve().parent
for pattern in LIB_GLOBS:
for candidate in sorted(pkg.glob(pattern)):
if candidate.is_file():
return candidate
return build_core(dest=dest or pkg)
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="Сборка libfgl-aircon (pyfglair)")
ap.add_argument("--inplace", action="store_true",
help="скопировать .so в каталог пакета pyfglair")
ap.add_argument("--dest", type=Path, help="каталог для копии .so")
ap.add_argument("--source", type=Path, help="корень монорепо")
ap.add_argument("--build-dir", type=Path, help="каталог сборки cmake")
ap.add_argument("--clean", action="store_true")
args = ap.parse_args(argv)
dest = args.dest
if args.inplace and dest is None:
dest = Path(__file__).resolve().parent
lib = build_core(
dest=dest, source=args.source, build_dir=args.build_dir,
clean=args.clean,
)
print(f"ядро: {lib}")
return 0
if __name__ == "__main__":
sys.exit(main())