Files
petr.polezhaev f9d917c66d ha(packaging): musllinux-wheel для HA OS и статический C++-рантайм
- _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
2026-09-29 16:20:21 +03:00

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())