"""Сборка libfgl-aircon.so из корня монорепо (cmake). Используется при упаковке wheel (setup.py) и для локальной разработки: python -m pyfglair._corebuild --inplace """ from __future__ import annotations import argparse import os import platform import re 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 musl_platform_tag() -> str | None: """musllinux-тег, если сборка идёт на musl (Alpine/HA OS), иначе None. wheel-тег `linux_` на musl вводит pip в заблуждение: он ставит glibc-сборку на Alpine, и dlopen падает на отсутствующем ld-linux-.so.2. На musl отдаём корректный `musllinux_X_Y_`. """ machine = platform.machine() try: result = subprocess.run( ["ldd", "--version"], capture_output=True, text=True, timeout=10 ) except (OSError, subprocess.SubprocessError): return None text = (result.stdout or "") + (result.stderr or "") if "musl" not in text.lower(): return None match = re.search(r"(\d+)\.(\d+)", text) major, minor = (match.group(1), match.group(2)) if match else ("1", "2") arch = {"amd64": "x86_64", "arm64": "aarch64"}.get(machine, machine) return f"musllinux_{major}_{minor}_{arch}" 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, bundled_mbedtls: bool | None = None, ) -> Path: """Собирает shared-ядро и (при dest) копирует его в каталог пакета. Для wheel mbedtls по умолчанию встраивается (bundled), чтобы .so не зависел от системной soname; FGL_BUNDLED_MBEDTLS=0 — использовать системный. """ src = Path(source) if source else source_root() if src is None: raise RuntimeError( "не найден корень fgl-aircon (CMakeLists.txt); задайте " "FGLAIR_SOURCE_ROOT" ) if bundled_mbedtls is None: env = os.environ.get("FGL_BUNDLED_MBEDTLS") bundled_mbedtls = env != "0" 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", "-DFGL_BUNDLED_MBEDTLS=" + ("ON" if bundled_mbedtls else "OFF"), ]) _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())