Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9d917c66d | ||
|
|
49d5533cd1 |
@@ -1,21 +1,17 @@
|
|||||||
# Сборка и публикация pyfglair в PyPI-реестр пакетов Gitea.
|
# Сборка и публикация pyfglair в PyPI-реестр пакетов Gitea.
|
||||||
# Запуск: Actions → Publish pyfglair → Run workflow (ручной, workflow_dispatch).
|
# Запуск: Actions → Publish pyfglair → Run workflow (workflow_dispatch).
|
||||||
#
|
#
|
||||||
# Авторизация (любой из вариантов):
|
# Собираются ДВА wheel (одинаковая версия):
|
||||||
# 1) встроенный job-токен: permissions: packages: write (Gitea ≥ 1.27 со
|
# * linux_x86_64 — glibc (HA Supervised/Core на Debian/Ubuntu);
|
||||||
# scope packages у features) — секреты не нужны;
|
# * musllinux_X_Y — musl (HA OS: Alpine-контейнеры); собирается в
|
||||||
# 2) personal access token (scope write:package) в секрете PYPI_TOKEN —
|
# контейнере python:3.12-alpine. Если runner не поддерживает
|
||||||
# для старых Gitea, где публикация пакетов job-токеном не реализована
|
# `container:` — job build-musllinux упадёт на apk; в этом случае
|
||||||
# (см. https://docs.gitea.com/usage/actions/comparison — "Package
|
# musllinux-wheel соберите вручную (см. docs/RELEASE_HA.md §1.4).
|
||||||
# repository authorization").
|
|
||||||
# Секрет PYPI_USER (опционально) — имя пользователя к PAT; по умолчанию актор.
|
|
||||||
# ВНИМАНИЕ: имена секретов не могут начинаться с GITEA_ (зарезервировано).
|
|
||||||
#
|
#
|
||||||
# Примечания:
|
# Авторизация: встроенный job-токен при `permissions: packages: write`
|
||||||
# * actions/checkout и actions/setup-python тянутся с github.com
|
# (Gitea ≥ 1.27) либо PAT write:package в секрете PYPI_TOKEN.
|
||||||
# (если инстанс изолирован — замените на абсолютные URL зеркал);
|
# Секрет PYPI_USER — имя пользователя к PAT; имена секретов не могут
|
||||||
# * сборка wheel с bundled mbedtls требует доступ к github.com
|
# начинаться с GITEA_ (префикс зарезервирован).
|
||||||
# (FetchContent); при BUNDLED=0 ставится системный libmbedtls-dev.
|
|
||||||
name: Publish pyfglair
|
name: Publish pyfglair
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -31,7 +27,7 @@ permissions:
|
|||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
build-glibc:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -56,7 +52,7 @@ jobs:
|
|||||||
cmake --version | head -1
|
cmake --version | head -1
|
||||||
g++ --version | head -1
|
g++ --version | head -1
|
||||||
|
|
||||||
- name: Build wheel + sdist
|
- name: Build wheel + sdist (glibc)
|
||||||
env:
|
env:
|
||||||
BUNDLED: ${{ inputs.bundled_mbedtls }}
|
BUNDLED: ${{ inputs.bundled_mbedtls }}
|
||||||
run: |
|
run: |
|
||||||
@@ -69,50 +65,71 @@ jobs:
|
|||||||
python -m build
|
python -m build
|
||||||
ls -l dist
|
ls -l dist
|
||||||
|
|
||||||
# Запускать из каталога без исходников: иначе python подхватывает
|
# Запуск из каталога без исходников: иначе python подхватывает
|
||||||
# ./pyfglair (без собранного .so) вместо установленного wheel.
|
# ./pyfglair (без собранного .so) вместо установленного wheel.
|
||||||
- name: Smoke test wheel
|
- name: Smoke test wheel (glibc)
|
||||||
run: |
|
run: |
|
||||||
python -m venv /tmp/wheeltest
|
python -m venv /tmp/wheeltest
|
||||||
WHEEL="$(pwd)/dist"
|
WHEEL="$(pwd)/dist"
|
||||||
/tmp/wheeltest/bin/pip install --quiet "$WHEEL"/*.whl
|
/tmp/wheeltest/bin/pip install --quiet "$WHEEL"/*.whl
|
||||||
cd /tmp
|
cd /tmp
|
||||||
/tmp/wheeltest/bin/python -P -c "import pyfglair.session as s; print('pyfglair:', s.__file__)"
|
/tmp/wheeltest/bin/python -P -c "import pyfglair.session as s; print('pyfglair(glibc):', s.__file__)"
|
||||||
/tmp/wheeltest/bin/python -P -c "from pyfglair import Template, Prop, convert_to_display; assert convert_to_display(Template.A, Prop.DISPLAY_TEMPERATURE, 7000) == 200"
|
/tmp/wheeltest/bin/python -P -c "from pyfglair import Template, Prop, convert_to_display; assert convert_to_display(Template.A, Prop.DISPLAY_TEMPERATURE, 7000) == 200"
|
||||||
|
|
||||||
- name: Publish to Gitea package registry (PyPI)
|
- name: Publish glibc wheel + sdist
|
||||||
env:
|
env:
|
||||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
SERVER: ${{ github.server_url }}
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
GITEA_JOB_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
PYPI_USER: ${{ secrets.PYPI_USER }}
|
PYPI_USER: ${{ secrets.PYPI_USER }}
|
||||||
JOB_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
run: bash scripts/publish-gitea-pypi.sh
|
||||||
|
|
||||||
|
build-musllinux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: python:3.12-alpine
|
||||||
|
steps:
|
||||||
|
- name: Build musllinux wheel
|
||||||
|
env:
|
||||||
|
BUNDLED: ${{ inputs.bundled_mbedtls }}
|
||||||
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
run: |
|
run: |
|
||||||
TOKEN="${PYPI_TOKEN:-$JOB_TOKEN}"
|
apk add --no-cache git cmake ninja g++ make curl libstdc++-dev
|
||||||
if [ -z "$TOKEN" ]; then
|
if [ -z "$REPO_URL" ] || [ "$REPO_URL" = "/.git" ]; then
|
||||||
echo "::error::Нет токена: разрешите 'packages: write' (Gitea ≥ 1.27) или задайте секрет PYPI_TOKEN (PAT write:package)"
|
echo "::error::Пустой github.server_url/repository — задайте контексты или соберите musllinux вручную (docs/RELEASE_HA.md §1.4)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
SERVER="${GITHUB_SERVER_URL:-}"
|
git clone --depth 1 "$REPO_URL" /src
|
||||||
if [ -z "$SERVER" ]; then
|
cd /src
|
||||||
SERVER="$(git remote get-url origin | sed -E 's#^git@([^:]+):.*#https://\1#; s#^(https?://[^/]+)/.*#\1#')"
|
if [ -n "$SHA" ]; then
|
||||||
|
git fetch --depth 1 origin "$SHA"
|
||||||
|
git checkout --detach FETCH_HEAD
|
||||||
fi
|
fi
|
||||||
REPO="${GITHUB_REPOSITORY:-$(git remote get-url origin | sed -E 's#.*[:/]([^/]+/[^/]+?)(\.git)?$#\1#')}"
|
if [ "$BUNDLED" = "0" ]; then
|
||||||
OWNER="${REPO%%/*}"
|
export FGL_BUNDLED_MBEDTLS=0
|
||||||
GITEA_USER="${PYPI_USER:-${GITHUB_ACTOR:-git}}"
|
else
|
||||||
pip install --upgrade twine
|
export FGL_BUNDLED_MBEDTLS=1
|
||||||
|
|
||||||
# twine 7 не поддерживает --skip-existing для не-PyPI репозиториев,
|
|
||||||
# поэтому проверяем simple-индекс Gitea сами.
|
|
||||||
VERSION="$(basename "$(ls dist/pyfglair-*.tar.gz | head -1)" | sed -E 's/^pyfglair-(.+)\.tar\.gz$/\1/')"
|
|
||||||
INDEX="$SERVER/api/packages/$OWNER/pypi/simple/pyfglair/"
|
|
||||||
if curl -fsS -u "$GITEA_USER:$TOKEN" "$INDEX" | grep -q "pyfglair-${VERSION}-"; then
|
|
||||||
echo "Версия $VERSION уже опубликована — пропуск загрузки."
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
pip install --upgrade build
|
||||||
|
python -m build --wheel
|
||||||
|
ls -l dist
|
||||||
|
|
||||||
python -m twine upload \
|
- name: Smoke test wheel (musl)
|
||||||
--repository-url "$SERVER/api/packages/$OWNER/pypi" \
|
run: |
|
||||||
--username "$GITEA_USER" \
|
cd /src
|
||||||
--password "$TOKEN" \
|
python -m venv /tmp/wheeltest
|
||||||
--non-interactive \
|
/tmp/wheeltest/bin/pip install --quiet "$(pwd)"/dist/*.whl
|
||||||
dist/*
|
cd /tmp
|
||||||
echo "Опубликовано: $SERVER/api/packages/$OWNER/pypi/simple/pyfglair/"
|
/tmp/wheeltest/bin/python -P -c "import pyfglair.session as s; print('pyfglair(musl):', s.__file__)"
|
||||||
|
/tmp/wheeltest/bin/python -P -c "from pyfglair import Template, Prop, convert_to_display; assert convert_to_display(Template.A, Prop.DISPLAY_TEMPERATURE, 7000) == 200"
|
||||||
|
|
||||||
|
- name: Publish musllinux wheel
|
||||||
|
env:
|
||||||
|
SERVER: ${{ github.server_url }}
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
GITEA_JOB_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
PYPI_USER: ${{ secrets.PYPI_USER }}
|
||||||
|
DIST_DIR: /src/dist
|
||||||
|
run: cd /src && bash scripts/publish-gitea-pypi.sh
|
||||||
|
|||||||
@@ -125,6 +125,14 @@ else()
|
|||||||
VERSION ${PROJECT_VERSION}
|
VERSION ${PROJECT_VERSION}
|
||||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||||
)
|
)
|
||||||
|
# Минимум внешних зависимостей у wheel: C++-рантайм встраиваем, остаются
|
||||||
|
# libc/libm (на musl — musl). Иначе Alpine/HA OS может не иметь
|
||||||
|
# libstdc++.so.6/libgcc_s.so.1 нужной сборки.
|
||||||
|
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||||
|
target_link_options(fgl-aircon-shared PRIVATE
|
||||||
|
-static-libstdc++ -static-libgcc
|
||||||
|
)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(FGL_BUILD_TESTS)
|
if(FGL_BUILD_TESTS)
|
||||||
|
|||||||
@@ -126,6 +126,38 @@ lanip_key_id) или импорт `config_*.json` от `fglair-discover`/`fglctl
|
|||||||
|
|
||||||
## Диагностика и известные ситуации
|
## Диагностика и известные ситуации
|
||||||
|
|
||||||
|
* **`OSError: cannot load library ... ld-linux-x86-64.so.2: No such file
|
||||||
|
or directory`** — на HA OS (Alpine/musl) установился glibc-wheel. Нужна
|
||||||
|
musllinux-сборка `pyfglair`: переустановите пакет из индекса (см.
|
||||||
|
[`docs/RELEASE_HA.md`](../../docs/RELEASE_HA.md) §1.3) и перезапустите HA.
|
||||||
|
* **«Не удалось загрузить мастер настройки: Invalid handler specified»** —
|
||||||
|
HA не смог импортировать компонент (обычно нет пакета `pyfglair`).
|
||||||
|
Проверьте лог HA:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
grep -iE "fglair|pyfglair" /config/home-assistant.log | tail -20
|
||||||
|
# ожидаемая причина: ModuleNotFoundError: No module named 'pyfglair'
|
||||||
|
```
|
||||||
|
|
||||||
|
Установите пакет в то же python-окружение, в котором работает HA, и
|
||||||
|
**полностью перезапустите** HA (не только интеграцию):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# HA OS / Supervised (SSH add-on с доступом к docker):
|
||||||
|
docker exec homeassistant python -m pip install --no-deps \
|
||||||
|
--index-url https://<user>:<token>@git.ratigorsk-12.ru/api/packages/<owner>/pypi/simple \
|
||||||
|
pyfglair
|
||||||
|
docker restart homeassistant
|
||||||
|
|
||||||
|
# HA Core в venv:
|
||||||
|
/srv/homeassistant/bin/python -m pip install --no-deps --index-url ... pyfglair
|
||||||
|
systemctl restart home-assistant@homeassistant
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка: `docker exec homeassistant python -c "import pyfglair; print(pyfglair.__version__)"` — должно печатать версию без ошибок.
|
||||||
|
Другие причины той же ошибки: компонент лежит не в
|
||||||
|
`<config>/custom_components/fglair/` (лишняя вложенность каталогов, нет
|
||||||
|
`__init__.py`), либо HA не перезапускался после копирования.
|
||||||
* **503 / оба слота заняты** — в LAN уже две сессии (телефон + ESPHome).
|
* **503 / оба слота заняты** — в LAN уже две сессии (телефон + ESPHome).
|
||||||
Освободите один слот и перезапустите интеграцию.
|
Освободите один слот и перезапустите интеграцию.
|
||||||
* **key_error** — LAN-ключ не совпал (`key_id` изменился). В HA появится
|
* **key_error** — LAN-ключ не совпал (`key_id` изменился). В HA появится
|
||||||
|
|||||||
+52
-3
@@ -29,9 +29,18 @@ Workflow: `.gitea/workflows/publish-pyfglair.yaml` (workflow объявляет
|
|||||||
|
|
||||||
Запуск: Actions → **Publish pyfglair** → Run workflow (вход
|
Запуск: Actions → **Publish pyfglair** → Run workflow (вход
|
||||||
`bundled_mbedtls`: `1` — встроить mbedtls, `0` — системный
|
`bundled_mbedtls`: `1` — встроить mbedtls, `0` — системный
|
||||||
`libmbedtls-dev`). Артефакт появится в
|
`libmbedtls-dev`). Собираются **два** wheel одной версии:
|
||||||
|
|
||||||
|
* `py3-none-linux_x86_64` — glibc (HA Supervised/Core на Debian/Ubuntu);
|
||||||
|
* `py3-none-musllinux_1_2_x86_64` — musl (**HA OS**, Alpine-контейнер);
|
||||||
|
job `build-musllinux` выполняется в `container: python:3.12-alpine`
|
||||||
|
(нужен `container:` у runner'а; иначе см. §1.4).
|
||||||
|
|
||||||
|
Артефакты появятся в
|
||||||
`https://git.ratigorsk-12.ru/api/packages/<owner>/pypi/simple/pyfglair/`
|
`https://git.ratigorsk-12.ru/api/packages/<owner>/pypi/simple/pyfglair/`
|
||||||
(`<owner>` — владелец репозитория, напр. `esphome`).
|
(`<owner>` — владелец репозитория, напр. `esphome`). C++-рантайм в .so
|
||||||
|
встроен (`-static-libstdc++ -static-libgcc`), поэтому wheel зависит только
|
||||||
|
от libc/libm соответствующей платформы.
|
||||||
|
|
||||||
Workflow собирает `wheel` + `sdist`, прогоняет smoke-тест установки
|
Workflow собирает `wheel` + `sdist`, прогоняет smoke-тест установки
|
||||||
(запускается из `/tmp` с `python -P`, чтобы не подхватить исходники
|
(запускается из `/tmp` с `python -P`, чтобы не подхватить исходники
|
||||||
@@ -42,6 +51,9 @@ Workflow собирает `wheel` + `sdist`, прогоняет smoke-тест
|
|||||||
|
|
||||||
### 1.2. Установка pyfglair в Home Assistant
|
### 1.2. Установка pyfglair в Home Assistant
|
||||||
|
|
||||||
|
Выбор wheel pip делает сам по платформе: на HA OS (Alpine/musl) — из
|
||||||
|
musllinux, на HA Supervised/Core (Debian/glibc) — из linux-сборки.
|
||||||
|
|
||||||
Пакет обязателен (cffi-биндинги). Варианты:
|
Пакет обязателен (cffi-биндинги). Варианты:
|
||||||
|
|
||||||
* **Вручную в окружение HA** (проще всего для теста; HA увидит
|
* **Вручную в окружение HA** (проще всего для теста; HA увидит
|
||||||
@@ -57,9 +69,46 @@ Workflow собирает `wheel` + `sdist`, прогоняет smoke-тест
|
|||||||
тогда HA сам поставит `pyfglair` из манифеста. Учтите риск dependency
|
тогда HA сам поставит `pyfglair` из манифеста. Учтите риск dependency
|
||||||
confusion при `--extra-index-url` — для внутреннего стенда допустимо.
|
confusion при `--extra-index-url` — для внутреннего стенда допустимо.
|
||||||
|
|
||||||
|
Если при добавлении интеграции HA пишет «Invalid handler specified» —
|
||||||
|
пакет `pyfglair` не установлен (или компонент не перезапущен). Смотрите
|
||||||
|
`grep -iE "fglair|pyfglair" /config/home-assistant.log`: при
|
||||||
|
`ModuleNotFoundError: No module named 'pyfglair'` установите пакет в
|
||||||
|
окружение HA (`docker exec homeassistant python -m pip install ...`) и
|
||||||
|
полностью перезапустите HA.
|
||||||
|
|
||||||
* **Публичный PyPI** — см. §2 (для распространения вне сети).
|
* **Публичный PyPI** — см. §2 (для распространения вне сети).
|
||||||
|
|
||||||
### 1.3. Обновление интеграции
|
### 1.3. Если HA уже установил glibc-wheel на HA OS
|
||||||
|
|
||||||
|
Симптом: `OSError: cannot load library ... Error loading shared library
|
||||||
|
ld-linux-x86-64.so.2: No such file or directory`. Переустановите пакет с
|
||||||
|
musllinux-сборкой:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker exec homeassistant python -m pip uninstall -y pyfglair
|
||||||
|
docker exec homeassistant python -m pip install --no-deps --index-url \
|
||||||
|
https://<user>:<token>@git.ratigorsk-12.ru/api/packages/<owner>/pypi/simple \
|
||||||
|
pyfglair
|
||||||
|
docker exec homeassistant python -c "import pyfglair; print(pyfglair.__version__)"
|
||||||
|
docker restart homeassistant
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4. Ручная сборка musllinux (если runner без `container:`)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm -v "$PWD":/src -w /src python:3.12-alpine sh -c '
|
||||||
|
apk add --no-cache git cmake ninja g++ make curl libstdc++-dev &&
|
||||||
|
pip install build &&
|
||||||
|
python -m build --wheel'
|
||||||
|
# dist/pyfglair-<ver>-py3-none-musllinux_1_2_x86_64.whl
|
||||||
|
|
||||||
|
pip install twine
|
||||||
|
python -m twine upload --repository-url \
|
||||||
|
https://git.ratigorsk-12.ru/api/packages/<owner>/pypi \
|
||||||
|
-u <user> -p <token> dist/*musllinux*.whl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.5. Обновление интеграции
|
||||||
|
|
||||||
`custom_components/fglair/` обновляется из Gitea (git pull / копирование),
|
`custom_components/fglair/` обновляется из Gitea (git pull / копирование),
|
||||||
затем перезапуск HA. `pyfglair` — переустановкой из пакетов Gitea.
|
затем перезапуск HA. `pyfglair` — переустановкой из пакетов Gitea.
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -22,6 +24,29 @@ LIB_GLOBS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
def source_root() -> Path | None:
|
||||||
"""Корень монорепо (CMakeLists.txt + include/fgl-aircon)."""
|
"""Корень монорепо (CMakeLists.txt + include/fgl-aircon)."""
|
||||||
env = os.environ.get("FGLAIR_SOURCE_ROOT")
|
env = os.environ.get("FGLAIR_SOURCE_ROOT")
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Публикация файлов из DIST_DIR в PyPI-реестр пакетов Gitea.
|
||||||
|
# Уже загруженные (по имени файла в simple-индексе) пропускаются: twine 7 не
|
||||||
|
# поддерживает --skip-existing для не-PyPI репозиториев.
|
||||||
|
#
|
||||||
|
# Переменные:
|
||||||
|
# TOKEN (или GITEA_JOB_TOKEN) — обязательный токен (PAT write:package или
|
||||||
|
# встроенный job-токен при packages: write)
|
||||||
|
# DIST_DIR — каталог с *.whl/*.tar.gz (по умолчанию dist)
|
||||||
|
# SERVER, OWNER — Gitea и владелец; если не заданы, берутся
|
||||||
|
# из git remote (origin)
|
||||||
|
# PYPI_USER — имя пользователя (по умолчанию актор)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIST_DIR="${DIST_DIR:-dist}"
|
||||||
|
TOKEN="${TOKEN:-${GITEA_JOB_TOKEN:-}}"
|
||||||
|
|
||||||
|
if [ -z "${SERVER:-}" ]; then
|
||||||
|
SERVER="$(git remote get-url origin \
|
||||||
|
| sed -E 's#^git@([^:]+):.*#https://\1#; s#^(https?://[^/]+)/.*#\1#')"
|
||||||
|
fi
|
||||||
|
if [ -z "${OWNER:-}" ]; then
|
||||||
|
REPO="${GITHUB_REPOSITORY:-$(git remote get-url origin \
|
||||||
|
| sed -E 's#.*[:/]([^/]+/[^/]+?)(\.git)?$#\1#')}"
|
||||||
|
OWNER="${REPO%%/*}"
|
||||||
|
fi
|
||||||
|
USER_NAME="${PYPI_USER:-${GITHUB_ACTOR:-git}}"
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "нет токена: задайте секрет PYPI_TOKEN (PAT write:package) или" >&2
|
||||||
|
echo "разрешите 'packages: write' для встроенного GITEA_TOKEN" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -d "$DIST_DIR" ]; then
|
||||||
|
echo "нет каталога $DIST_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INDEX="$SERVER/api/packages/$OWNER/pypi/simple/pyfglair/"
|
||||||
|
pip install --upgrade --quiet twine
|
||||||
|
|
||||||
|
for FILE in "$DIST_DIR"/*; do
|
||||||
|
NAME="$(basename "$FILE")"
|
||||||
|
if curl -fsS -u "$USER_NAME:$TOKEN" "$INDEX" 2>/dev/null \
|
||||||
|
| grep -qF "$NAME"; then
|
||||||
|
echo "уже опубликовано: $NAME"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
python -m twine upload \
|
||||||
|
--repository-url "$SERVER/api/packages/$OWNER/pypi" \
|
||||||
|
--username "$USER_NAME" \
|
||||||
|
--password "$TOKEN" \
|
||||||
|
--non-interactive \
|
||||||
|
"$FILE"
|
||||||
|
echo "опубликовано: $NAME"
|
||||||
|
done
|
||||||
|
echo "индекс: $INDEX"
|
||||||
@@ -14,15 +14,18 @@ except ImportError: # setuptools < 70
|
|||||||
from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
|
from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
_COREBUILD = None
|
||||||
|
|
||||||
|
|
||||||
def _load_corebuild():
|
def _load_corebuild():
|
||||||
spec = spec_from_file_location(
|
global _COREBUILD
|
||||||
"_pyfglair_corebuild", ROOT / "pyfglair" / "_corebuild.py"
|
if _COREBUILD is None:
|
||||||
)
|
spec = spec_from_file_location(
|
||||||
module = module_from_spec(spec)
|
"_pyfglair_corebuild", ROOT / "pyfglair" / "_corebuild.py"
|
||||||
spec.loader.exec_module(module)
|
)
|
||||||
return module
|
_COREBUILD = module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(_COREBUILD)
|
||||||
|
return _COREBUILD
|
||||||
|
|
||||||
|
|
||||||
class build_py(_build_py):
|
class build_py(_build_py):
|
||||||
@@ -46,10 +49,17 @@ class BinaryDistribution(Distribution):
|
|||||||
|
|
||||||
|
|
||||||
class bdist_wheel(_bdist_wheel):
|
class bdist_wheel(_bdist_wheel):
|
||||||
"""Тег py3-none-<plat>: .so собран в ABI-режиме cffi (не CPython-ABI)."""
|
"""Тег py3-none-<plat>: .so собран в ABI-режиме cffi (не CPython-ABI).
|
||||||
|
|
||||||
|
На musl (Alpine/HA OS) платформа заменяется на musllinux_X_Y_<arch>,
|
||||||
|
иначе pip поставит glibc-сборку и dlopen упадёт.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_tag(self):
|
def get_tag(self):
|
||||||
_, _, platform = super().get_tag()
|
_, _, platform = super().get_tag()
|
||||||
|
musl = _load_corebuild().musl_platform_tag()
|
||||||
|
if musl is not None and platform.startswith("linux_"):
|
||||||
|
platform = musl
|
||||||
return "py3", "none", platform
|
return "py3", "none", platform
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""Определение платформы wheel: musl (Alpine/HA OS) vs glibc."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pyfglair import _corebuild
|
||||||
|
|
||||||
|
|
||||||
|
class _Result:
|
||||||
|
def __init__(self, stdout: str = "", stderr: str = "") -> None:
|
||||||
|
self.stdout = stdout
|
||||||
|
self.stderr = stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_glibc_returns_none(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
_corebuild.subprocess,
|
||||||
|
"run",
|
||||||
|
lambda *args, **kwargs: _Result("ldd (GNU libc) 2.42\n"),
|
||||||
|
)
|
||||||
|
assert _corebuild.musl_platform_tag() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_musl_x86_64(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
_corebuild.subprocess,
|
||||||
|
"run",
|
||||||
|
lambda *args, **kwargs: _Result("musl libc (x86_64)\nVersion 1.2.5\n"),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(_corebuild.platform, "machine", lambda: "x86_64")
|
||||||
|
assert _corebuild.musl_platform_tag() == "musllinux_1_2_x86_64"
|
||||||
|
|
||||||
|
|
||||||
|
def test_musl_aarch64(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
_corebuild.subprocess,
|
||||||
|
"run",
|
||||||
|
lambda *args, **kwargs: _Result(
|
||||||
|
"musl libc (aarch64)\nVersion 1.2.4\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(_corebuild.platform, "machine", lambda: "aarch64")
|
||||||
|
assert _corebuild.musl_platform_tag() == "musllinux_1_2_aarch64"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ldd_missing(monkeypatch):
|
||||||
|
def boom(*args, **kwargs):
|
||||||
|
raise FileNotFoundError("ldd")
|
||||||
|
|
||||||
|
monkeypatch.setattr(_corebuild.subprocess, "run", boom)
|
||||||
|
assert _corebuild.musl_platform_tag() is None
|
||||||
Reference in New Issue
Block a user