Files
Pluto-SDR/docs/CI.md

188 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CI.md — офлайн-автотесты на self-hosted Gitea Actions
Сервер Gitea изолирован (без постоянного интернета), поэтому здесь нет
`actions/checkout`, нет Marketplace-экшенов и нет ни одного `uses:` — только
`run:`. Прежде чем править `.gitea/workflows/*`, прочитай это целиком: почти
все неожиданные падения объясняются структурой раздела «Нюансы окружения»
ниже, а не багом в тесте.
## Два runner'а — зачем и как разделены
Сигнальный тракт pluto-link целиком живёт на ARM (Cortex-A9 внутри Pluto), а
сам Gitea-сервер — x86_64. Проверить можно только то, что не требует
Pluto-железа: (1) чистую логику на libc, (2) факт компиляции под armhf,
(3) статические grep-правила. Для этого заведено два независимых runner'а с
разными метками — они физически разные Docker-контейнеры на одном сервере:
| Runner | Контейнер | База образа | Метка | Что гоняет |
|---|---|---|---|---|
| основной | `gitea_runner` | Alpine `act_runner_tools:0.2.13` | `host` | 01-smoke, 02-guardrails |
| cross-builder | `gitea_runner_armhf` | Debian bookworm, **нативный x86-64** | `armhf` | 03-gftest, 04-cross-build |
У каждого `capacity: 1` в `config.yaml` — внутри одного runner'а job'ы идут
строго последовательно (см. «Нюансы» ниже про параллелизм между runner'ами).
Оба используют host-executor: шаги `run:` выполняются прямо в окружении
процесса runner (то есть внутри контейнера), без docker-in-docker. Отсюда то
же правило, что и в примерах для других проектов на этом сервере: **что
лежит в образе runner'а — то и доступно job'у; что стоит на Debian-хосте
сервера — не видно job'у вообще.**
## Что проверяет каждый workflow
### `.gitea/workflows/01-smoke.yml` — информационный
Печатает `/etc/os-release` и `uname -a` (докажет, что это Alpine job-окружения,
а не Debian-хост сервера), какие инструменты есть (`git`/`bash`/`cc`/`make`/
`arm-linux-gnueabihf-gcc`) и есть ли `$XARM/lib/*.a`. **Никогда не падает**
только помечает `[ЕСТЬ]`/`[НЕТ]`. Зелёный означает буквально одно: «runner
жив и взял задачу», не более. `timeout-minutes: 5`.
### `.gitea/workflows/02-guardrails.yml` — статический страж
Клонирует репозиторий (без `actions/checkout`, см. ниже) и гоняет
`scripts/ci_guardrails.sh` — 17 grep/bash-ассертов на жёсткие правила
CLAUDE.md: sample rate `1920000` и никогда `3840000`, `header_len=12` на TX
**и** RX (техдолг #1), только `local:` в тракте, статические/ARM-флаги в
makefile, отсутствие тяжёлых зависимостей, отсутствие Python в `src/`. Скрипт
самодостаточен и гоняется точно так же локально:
```sh
scripts/ci_guardrails.sh
```
Тулчейн не нужен — работает на базовом Alpine-образе. `timeout-minutes: 5`.
### `.gitea/workflows/03-gftest.yml` — юнит-тест group-FEC
`make gftest` (см. `makefile`): нативная (не кросс-!) сборка
`src/group_fec.c` с `-DGF_SELFTEST` через хостовый `cc` cross-builder'а и
запуск самопроверки GF(256) — 774 400 кейсов восстановления группового FEC
16+2, честный pass/fail по коду возврата. Модуль тянет только `<string.h>/
<stdint.h>/<stdio.h>/<stdlib.h>`, поэтому компилируется любым gcc. Идёт на
`runs-on: armhf`, потому что там гарантированно есть нативный `cc`
(`build-essential`) — это не про ARM, просто удобно совместить с
cross-builder'ом. `timeout-minutes: 5`.
### `.gitea/workflows/04-cross-build.yml` — приёмочный тест ARM-тулчейна
`make check-env && make all` — реальная кросс-сборка `transmitter`,
`receiver`, `ofdm_bench` под armhf с `-Wall -Wextra -static`. Ловит любую
компиляционную регрессию в боевом коде до деплоя на плату. `timeout-minutes: 15`.
## Ключевой архитектурный момент: это НЕ эмуляция ARM
Первая попытка поднять второй runner использовала `platform: linux/arm64` /
`linux/arm/v7` в compose — и падала с `exec /sbin/tini: exec format error`:
Docker пытался выполнить ARM-бинарь на x86-ядре сервера без
qemu/binfmt-регистрации.
Это была неверная модель. **Кросс-компиляция не требует ARM-машины.**
`arm-linux-gnueabihf-gcc` — обычный **x86-бинарь**, который на входе берёт
`.c` и на выходе даёт ARM-машинный код; выполняется на полной скорости
x86-ядра, без единого такта эмуляции. Это ровно то же самое, что уже
десятилетиями делает Raspberry Pi-хост в `scripts/build_deps.sh` — только
хост там aarch64, а не x86_64, что для кросс-компиляции роли не играет.
Рабочее решение: `gitea_runner_armhf`**нативный x86-64** контейнер
(`FROM debian:bookworm`, без `platform:` в compose), в который *вложен*
ARM-кросс-тулчейн как обычный пакет (`gcc-arm-linux-gnueabihf`). Сам
`act_runner` в этом контейнере тоже нативный `linux-amd64` — ARM в цепочке
нет нигде, кроме результата компиляции.
## Нюансы окружения (то, что реально ломалось)
**musl vs glibc — «файл есть» ≠ «работает».** На Alpine (`host`-runner)
glibc-бинарь может лежать на диске и даже отвечать на `command -v`, но
падать с обманчивым `not found` при запуске — потому что нет `ld-linux`.
Поэтому все приёмочные шаги здесь **компилируют и запускают** пробную
программу, а не проверяют `command -v`. В `04-cross-build.yml` это доведено
до предела: после сборки пробника парсится байт `e_machine` прямо из
ELF-заголовка (`od -An -tx1 -j18 -N2`, ждём `2800` = `EM_ARM` в
little-endian) — «собралось без ошибки» тоже не значит «собралось под ту
архитектуру». Тот же разбор повторяется для всех трёх собранных бинарников в
`04`, а не только для пробника.
**`$XARM` перенесён с RPi, не пересобирается в CI.** Пять статических
armhf-библиотек (`libliquid.a`, `libfec.a`, `libfftw3f.a`, `libiio.a`,
`libad9361.a`) — результат `scripts/build_deps.sh` (~1520 мин, нужен
интернет и sudo). Пересобирать их в образе cross-builder'а на каждый билд
образа избыточно: они `.a`-файлы с ARM-машинным кодом, архитектура и ОС
хоста, на котором их скопировали, роли не играет. Поэтому в
`runner-image/Dockerfile` (лежит на сервере, `/opt/gitea-runner-armhf/`, вне
этого git-репозитория) готовый `~/xarm` с RPi просто кладётся `COPY xarm
/root/xarm` — путь совпадает с дефолтным `XARM ?= $(HOME)/xarm` из
`makefile`, поэтому `make check-env`/`make all` в CI работают без
дополнительных переменных окружения.
**Циклические зависимости статических либ потребовали group-linking.**
Прямой прогон `make all` на реальном cross-builder'е (не на бумаге) вскрыл,
что однопроходного порядка `-lad9361 -liio -lliquid -lfec -lfftw3f` иногда
недостаточно — компоновщик требует повторных проходов по кругу
`ad9361↔iio`. Исправлено в `makefile` через `-Wl,--start-group ... -Wl,--end-group`
вокруг всего списка библиотек (коммит `738fd4a`): линкер пересматривает
группу, пока не разрешит все символы, вместо одного прохода слева направо.
Это именно то, ради чего 04-cross-build существует — баг был невидим на
бумаге и всплыл только на реальной кросс-сборке в CI.
**Параллелизм: между runner'ами есть, внутри — нет.** `capacity: 1` у
каждого runner'а сериализует job'ы **в пределах одного контейнера**, но
`host` и `armhf` — независимые процессы с раздельной очередью, поэтому
01/02 (на `host`) и 03/04 (на `armhf`) в реальности выполняются
параллельно друг другу; наблюдалось по чередованию task-id в логах push'а.
Внутри `armhf` 03 и 04 всегда строго последовательны (04 ждёт освобождения
runner'а после 03).
**Клон репозитория — без `actions/checkout`.** Marketplace-экшены
недоступны офлайн. Вместо этого каждый job сам клонирует себя через
токен job'а:
```yaml
- name: Получить код репозитория
env:
REPO_TOKEN: ${{ github.token }}
run: |
rm -rf repo
git clone --quiet "http://x-access-token:${REPO_TOKEN}@localhost:3000/${GITHUB_REPOSITORY}.git" repo
git -C repo checkout --quiet "$GITHUB_SHA"
```
`--quiet` обязателен: без него git печатает URL с токеном в progress-вывод,
который попадает в открытые логи Actions.
**Уборка рабочего каталога — `if: always()`.** Оба runner'а — общие
контейнеры для всех job'ов; мусор от упавшего шага иначе копится в ФС
контейнера до пересоздания.
**Токен регистрации в логах — разовая гигиена.** `GITEA_RUNNER_REGISTRATION_TOKEN`
из `docker-compose.runner-armhf.yml` виден в истории команд/логах настройки.
Он однократный (используется только при первой регистрации — `data/.runner`
уже существует), но по итогам стоит сбросить в Gitea (Site Administration →
Actions → Runners → сброс токена регистрации), чтобы старое значение не
годилось для привязки постороннего runner'а к инстансу.
## Как гонять локально, без runner'а
```sh
scripts/ci_guardrails.sh # 02 — только bash/grep, где угодно
make gftest # 03 — нужен любой hostовый cc
make check-env && make all # 04 — нужен arm-linux-gnueabihf-gcc + $XARM
# (например, прямо на RPi-хосте)
```
## Что сознательно не автоматизировано
- `scripts/test_file_link.sh` — e2e-передача файла через радио с md5-сверкой:
нужны две живые Pluto-платы по SSH/RF.
- `scripts/deploy.sh`, `make deploy` — заливка бинарников на платы.
- On-board `ofdm_bench`, PER-свипы, соук-тесты — числа с x86 бессмысленны:
бюджет CPU в CLAUDE.md посчитан для Cortex-A9 @ 667 МГц, а не для сервера.
Правило #6 в CLAUDE.md: при изменении PHY-параметров замерять на плате и
обновлять `docs/BENCHMARK.md` в том же коммите — CI это не проверяет.
Зелёные 0104 подтверждают компилируемость, статические инварианты и
group-FEC математику. Они **не** подтверждают, что радиолиния действительно
передаёт данные по эфиру — это по-прежнему ручной чек-лист выше.
## Файлы
- `.gitea/workflows/{01-smoke,02-guardrails,03-gftest,04-cross-build}.yml`
- `scripts/ci_guardrails.sh` — логика 02, гоняется и локально
- `runner-image/Dockerfile`, `docker-compose.runner-armhf.yml`,
`entrypoint.sh` cross-builder'а**вне этого репозитория**, на сервере в
`/opt/gitea-runner-armhf/`. Не версионируются здесь; если понадобится
восстановить cross-builder с нуля, конфигурация существует только на
сервере.