Files
Pluto-SDR/docs/CI.md

14 KiB
Raw Blame History

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/. Скрипт самодостаточен и гоняется точно так же локально:

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'а:

- 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'а

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 с нуля, конфигурация существует только на сервере.