- _corebuild.musl_platform_tag(): на musl (Alpine/HA OS) bdist_wheel выпускает py3-none-musllinux_X_Y_<arch> вместо linux_<arch> — иначе pip ставит glibc-сборку и dlopen падает на ld-linux-x86-64.so.2 - shared-библиотека линкуется со -static-libstdc++ -static-libgcc (gcc): NEEDED только libm/libc (проверено readelf), меньше зависимостей у HA OS - Gitea Actions: job build-musllinux (container python:3.12-alpine) собирает и публикует musl-wheel; build-glibc — как раньше; общий scripts/publish-gitea-pypi.sh с пропуском уже загруженных файлов - тесты musl_platform_tag; RELEASE_HA §1.1-1.4 (выбор wheel, переустановка на HA OS, ручная сборка musllinux без container:); README troubleshooting
168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
"""Сборка 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_<arch>` на musl вводит pip в заблуждение: он ставит
|
|
glibc-сборку на Alpine, и dlopen падает на отсутствующем
|
|
ld-linux-<arch>.so.2. На musl отдаём корректный `musllinux_X_Y_<arch>`.
|
|
"""
|
|
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())
|