diff --git a/.gitea/workflows/01-smoke.yml b/.gitea/workflows/01-smoke.yml new file mode 100644 index 0000000..8ccb6cd --- /dev/null +++ b/.gitea/workflows/01-smoke.yml @@ -0,0 +1,63 @@ +# 01-smoke — информационный workflow. +# Доказывает только одно: runner жив и берёт задачи. Не падает от отсутствия +# инструмента — печатает "НЕТ" и идёт дальше. Работает на базовом образе +# runner (git+bash), пересборка не нужна. + +name: 01-smoke + +on: + push: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + smoke: + runs-on: host + timeout-minutes: 5 + + steps: + - name: Окружение job + run: | + echo "=== ОС job (не хоста!) ===" + cat /etc/os-release 2>/dev/null || echo "нет /etc/os-release" + uname -a + + - name: Доступные инструменты + run: | + echo "=== Базовые ===" + for t in git bash sh; do + if command -v "$t" >/dev/null 2>&1; then + printf ' [ЕСТЬ] %s (%s)\n' "$t" "$(command -v "$t")" + else + printf ' [НЕТ] %s\n' "$t" + fi + done + + echo "" + echo "=== Сборка (нужны 03-gftest/04-cross-build) ===" + for t in cc make arm-linux-gnueabihf-gcc; do + if command -v "$t" >/dev/null 2>&1; then + printf ' [ЕСТЬ] %s\n' "$t" + else + printf ' [НЕТ] %s (см. docs/RUNNER-IMAGE.md, если такой файл заведён)\n' "$t" + fi + done + + - name: Кросс-либы $XARM (нужны 04-cross-build) + run: | + XARM="${XARM:-$HOME/xarm}" + echo "XARM=$XARM" + for lib in libliquid.a libfec.a libad9361.a libiio.a libfftw3f.a; do + if [ -f "$XARM/lib/$lib" ]; then + printf ' [ЕСТЬ] %s\n' "$lib" + else + printf ' [НЕТ] %s\n' "$lib" + fi + done diff --git a/.gitea/workflows/02-guardrails.yml b/.gitea/workflows/02-guardrails.yml new file mode 100644 index 0000000..4357454 --- /dev/null +++ b/.gitea/workflows/02-guardrails.yml @@ -0,0 +1,42 @@ +# 02-guardrails — проверочный workflow. +# Статические grep-проверки жёстких правил CLAUDE.md (rate 1920000, +# header_len=12 на TX и RX, только local:, static/ARM-флаги, нет тяжёлых +# зависимостей). Логика — в scripts/ci_guardrails.sh, тот же скрипт можно +# гонять локально. Только git+bash, пересборка образа runner не нужна. + +name: 02-guardrails + +on: + push: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + guardrails: + runs-on: host + timeout-minutes: 5 + + steps: + - 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" + + - name: Прогнать guardrails + run: | + chmod +x repo/scripts/ci_guardrails.sh + repo/scripts/ci_guardrails.sh + + - name: Убрать за собой + if: always() + run: rm -rf repo diff --git a/.gitea/workflows/03-gftest.yml b/.gitea/workflows/03-gftest.yml new file mode 100644 index 0000000..a936d64 --- /dev/null +++ b/.gitea/workflows/03-gftest.yml @@ -0,0 +1,61 @@ +# 03-gftest — проверочный workflow. +# Нативная (не кросс-) сборка group_fec.c с -DGF_SELFTEST через хостовый cc +# (makefile: цель gftest) и прогон самопроверки GF(256): 774 400 кейсов +# восстановления группового FEC 16+2. Модуль тянет только / +# // — компилируется и на musl. +# +# Требует в образе runner: build-base (cc) + make. Пока их нет — job падает +# на шаге "Проверить инструменты" с понятным сообщением, а не посреди сборки. + +name: 03-gftest + +on: + push: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + gftest: + runs-on: host + timeout-minutes: 5 + + steps: + - name: Проверить инструменты (реальная компиляция, не command -v) + # На Alpine/musl наличие файла ничего не доказывает (см. RUNNER-IMAGE.md + # про glibc-бинари без ld-linux) — поэтому здесь компилируем и запускаем + # крошечный .c, а не просто ищем cc/make на PATH. + run: | + for t in cc make; do + command -v "$t" >/dev/null 2>&1 || { + echo "ОШИБКА: '$t' нет в образе runner — apk add build-base make в runner-image/Dockerfile" + exit 1 + } + done + tmp=$(mktemp -d) + printf 'int main(void){return 42-42;}\n' > "$tmp/probe.c" + cc -O2 "$tmp/probe.c" -o "$tmp/probe" + "$tmp/probe" + echo "OK: cc реально компилирует и линкует исполняемый файл" + rm -rf "$tmp" + + - 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" + + - name: make gftest (774 400 кейсов GF(256)) + run: make -C repo gftest + + - name: Убрать за собой + if: always() + run: rm -rf repo diff --git a/.gitea/workflows/04-cross-build.yml b/.gitea/workflows/04-cross-build.yml new file mode 100644 index 0000000..ffb5e4e --- /dev/null +++ b/.gitea/workflows/04-cross-build.yml @@ -0,0 +1,103 @@ +# 04-cross-build — проверочный workflow. Приёмочный тест ARM-тулчейна в +# образе runner: пока он не заведён — job закономерно красный, и это его +# основная роль (по аналогии с их 04-toolchain). +# +# arm-linux-gnueabihf-gcc и статические либы $XARM собраны под glibc +# (build_deps.sh гоняется на Debian), а базовый образ act_runner — Alpine/ +# musl. glibc-хостовый cross-gcc на musl не запустится вовсе, либо запустится, +# но даст бинарь, слинкованный не тем, чем ожидается — поэтому "файл лежит" +# и "command -v нашёл" ничего не доказывают (см. RUNNER-IMAGE.md про +# обманчивый "not found" без ld-linux). Доказательство здесь только одно: +# реальная кросс-компиляция крошечной программы + проверка машинного типа +# результата по ELF-заголовку (e_machine == EM_ARM), а затем настоящая +# сборка tx/rx/bench штатным make all. + +name: 04-cross-build + +on: + push: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + cross-build: + runs-on: host + timeout-minutes: 15 + + steps: + - name: Проверить кросс-тулчейн (реальная ARM-компиляция, не command -v) + run: | + if ! command -v arm-linux-gnueabihf-gcc >/dev/null 2>&1; then + echo "ОШИБКА: arm-linux-gnueabihf-gcc нет в образе runner." + echo "Нужен отдельный flavor образа (Debian-база с gcc-arm-linux-gnueabihf" + echo "или Alpine + релокейтный prebuilt-тулчейн) — см. план в docs/." + exit 1 + fi + + tmp=$(mktemp -d) + printf 'int main(void){return 0;}\n' > "$tmp/hello.c" + + if ! arm-linux-gnueabihf-gcc -static -o "$tmp/hello" "$tmp/hello.c" 2>"$tmp/err.log"; then + echo "ОШИБКА: arm-linux-gnueabihf-gcc не смог собрать даже hello world." + echo "Типичная причина на Alpine/musl: сам gcc — glibc-бинарь без ld-linux." + cat "$tmp/err.log" + rm -rf "$tmp" + exit 1 + fi + + # e_machine — байты 18-19 ELF-заголовка (little-endian uint16). + # EM_ARM = 0x0028 → байты "28 00". + machine=$(od -An -tx1 -j 18 -N 2 "$tmp/hello" | tr -d ' \n') + if [ "$machine" != "2800" ]; then + echo "ОШИБКА: результат не ARM (e_machine=0x$machine, ожидали 0x0028)." + echo "Тулчейн собирает не под ту архитектуру — заведён неверный flavor образа." + rm -rf "$tmp" + exit 1 + fi + echo "OK: arm-linux-gnueabihf-gcc реально собрал ARM-бинарь (e_machine=0x$machine)" + rm -rf "$tmp" + + - 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" + + - name: make check-env ($XARM статические либы) + run: make -C repo check-env + + - name: make all (transmitter, receiver, ofdm_bench под armhf) + run: | + make -C repo all 2>&1 | tee /tmp/build.log + echo "" + echo "=== Проверка архитектуры собранных бинарников ===" + for bin in transmitter receiver ofdm_bench; do + machine=$(od -An -tx1 -j 18 -N 2 "repo/build/$bin" | tr -d ' \n') + if [ "$machine" != "2800" ]; then + echo "ОШИБКА: repo/build/$bin не ARM (e_machine=0x$machine)" + exit 1 + fi + echo " [OK] $bin — ARM-бинарь (e_machine=0x$machine)" + done + + - name: Нет предупреждений компилятора (-Wall -Wextra уже в CFLAGS) + run: | + if grep -q "warning:" /tmp/build.log; then + echo "ОШИБКА: сборка прошла с предупреждениями:" + grep "warning:" /tmp/build.log + exit 1 + fi + echo "OK: предупреждений нет" + + - name: Убрать за собой + if: always() + run: rm -rf repo /tmp/build.log diff --git a/scripts/ci_guardrails.sh b/scripts/ci_guardrails.sh new file mode 100644 index 0000000..64e6040 --- /dev/null +++ b/scripts/ci_guardrails.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# ci_guardrails.sh — статические grep-проверки жёстких правил CLAUDE.md. +# Ловит регрессии до кросс-сборки и до платы: без тулчейна, без $XARM. +# Запуск: scripts/ci_guardrails.sh (локально или из .gitea/workflows/02-guardrails.yml) +set -euo pipefail +cd "$(dirname "$0")/.." + +pass=0 +fail=0 + +check() { + description="$1" + if eval "$2" >/dev/null 2>&1; then + printf ' [OK] %s\n' "$description" + pass=$((pass + 1)) + else + printf ' [FAIL] %s\n' "$description" + fail=$((fail + 1)) + fi +} + +echo "== Правило #1: язык C, без Python/GNU Radio в тракте ==" +check "нет .py в src/" '[ -z "$(find src -name "*.py")" ]' + +echo "" +echo "== Правило #2: libiio только local: в тракте ==" +check "нет буквального \"ip: в src/*.c" '! grep -rn "\"ip:" src/*.c' +check "TX_URI_DEFAULT = local:" 'grep -q "TX_URI_DEFAULT.*\"local:\"" src/common.h' +check "RX_URI_DEFAULT = local:" 'grep -q "RX_URI_DEFAULT.*\"local:\"" src/common.h' + +echo "" +echo "== Правило #3: статическая линковка, ARM-флаги (makefile) ==" +check "-static" 'grep -q -- "-static" makefile' +check "-mcpu=cortex-a9" 'grep -q -- "-mcpu=cortex-a9" makefile' +check "-mfpu=neon" 'grep -q -- "-mfpu=neon" makefile' +check "-mfloat-abi=hard" 'grep -q -- "-mfloat-abi=hard" makefile' +check "-ffast-math" 'grep -q -- "-ffast-math" makefile' + +echo "" +echo "== Правило #4: нет тяжёлых зависимостей ==" +check "нет cjson/curl/ffmpeg/libav в src/" \ + '! grep -rniE "cjson|curl/curl\.h|libav|ffmpeg" src/*.c src/*.h' + +echo "" +echo "== Правило #5: sample rate 1920000, никогда 3840000 ==" +check "DEFAULT_RATE = 1920000 в common.h" 'grep -q "DEFAULT_RATE.*1920000" src/common.h' +check "нет 3840000 нигде в src/" '! grep -rn "3840000" src/' + +echo "" +echo "== Формат кадра: header_len=12 на обеих сторонах (техдолг #1) ==" +check "HDR_SIZE = 12 в common.h" 'grep -q "define HDR_SIZE *12" src/common.h' +check "ofdmflexframegen_set_header_len(fg, HDR_SIZE) в transmitter.c" \ + 'grep -q "ofdmflexframegen_set_header_len(fg, HDR_SIZE)" src/transmitter.c' +check "ofdmflexframesync_set_header_len(fs, HDR_SIZE) в receiver.c" \ + 'grep -q "ofdmflexframesync_set_header_len(fs, HDR_SIZE)" src/receiver.c' +check "сигнатура заголовка 0xF0/0xAA в common.h" \ + 'grep -q "HDR_SIGNATURE_0 *0xF0" src/common.h' +check "сигнатура заголовка 0xF0/0xAA (байт 1) в common.h" \ + 'grep -q "HDR_SIGNATURE_1 *0xAA" src/common.h' + +echo "" +echo "== Итог ==" +printf 'Пройдено: %d, Провалено: %d\n' "$pass" "$fail" + +[ "$fail" -eq 0 ]