From 2e84ed764979d7ec7de6bc07a0cf2e9e92d7822e Mon Sep 17 00:00:00 2001 From: Dmitry Ermakov <7954832+dimerr@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:46:29 +0000 Subject: [PATCH 1/2] ci, docs: build, smoke-test and release openipc-novatek build-novatek.yml is build-anyka.yml for openipc-novatek's NT98562 and NT98566: one build leg per SoC (with gcc-arm-none-eabi for the loader), the 256 KiB partition check, two boots of each image under qemu-hisilicon's nt98562/nt98566 machines as a hard gate, release to the novatek tag, publish to OpenIPC/firmware latest. The smoke test needs a qemu-hisilicon whose loader stand-in unpacks LZMA BCL1 containers. README and CLAUDE.md document the family. --- .github/workflows/build-novatek.yml | 355 ++++++++++++++++++++++++++++ CLAUDE.md | 26 ++ README.md | 59 +++++ 3 files changed, 440 insertions(+) create mode 100644 .github/workflows/build-novatek.yml diff --git a/.github/workflows/build-novatek.yml b/.github/workflows/build-novatek.yml new file mode 100644 index 000000000000..648946ad810c --- /dev/null +++ b/.github/workflows/build-novatek.yml @@ -0,0 +1,355 @@ +name: Build novatek + +on: + # Pull requests against this branch change the workflow itself; they + # build the current openipc-novatek. Code changes reach it through + # workflow_call from the caller on openipc-novatek, which passes the revision + # to build. Only workflow_dispatch can release or publish. + pull_request: + branches: [openipc] + paths: + - .github/workflows/build-novatek.yml + workflow_call: + inputs: + ref: + description: openipc-novatek revision to build + type: string + required: true + workflow_dispatch: + inputs: + release: + description: Publish to this repo's Novatek release + type: boolean + default: false + publish: + description: Publish to OpenIPC/firmware latest and report to openipc.org + type: boolean + default: false + +# The shape of build-hisilicon.yml: +# build (matrix, every SoC) -> qemu (prep) -> qemu_smoke (hard gate) -> +# release (own, `release` input) + publish (OpenIPC/firmware latest + +# openipc.org report, `publish` input). +# build.sh takes SoCs as arguments; the matrix calls it with one SoC per leg. + +jobs: + # Resolve the openipc-novatek revision this run builds from. The workflow + # lives on the openipc branch, so github.sha is the openipc commit, not + # the revision being built; release provenance must name the commit that + # build.sh actually compiled. + resolve: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + sha: ${{ steps.rev.outputs.sha }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || 'openipc-novatek' }} + path: src + + - name: Resolve revision + id: rev + run: echo "sha=$(git -C src rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + build: + needs: resolve + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - { soc: nt98562, files: "output/u-boot-nt98562-*.bin" } + - { soc: nt98566, files: "output/u-boot-nt98566-*.bin" } + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # The commit resolve recorded, not the ref again: every leg then + # builds the revision the release and openipc.org report name, + # even if the branch moves while the matrix runs. + ref: ${{ needs.resolve.outputs.sha }} + + - name: Install dependencies + timeout-minutes: 15 + run: | + # apt mirrors on hosted runners intermittently HANG on the + # mirrorlist fetch (not just fail). Bound each attempt with its own + # `timeout` so a hang is killed fast and the retry actually fires. + install() { + sudo timeout 180 apt-get update && \ + sudo timeout 240 apt-get install -y --no-install-recommends \ + build-essential gcc-arm-linux-gnueabi gcc-arm-none-eabi \ + device-tree-compiler \ + bison flex bc libssl-dev python3 + } + for i in 1 2 3 4; do + install && break + [ "$i" = 4 ] && { echo "apt failed after 4 attempts" >&2; exit 1; } + echo "apt attempt $i failed/timed out; retrying in $((i*10))s" >&2 + sleep $((i*10)) + done + + - name: Build ${{ matrix.soc }} + run: bash build.sh ${{ matrix.soc }} + + - name: Check partition fit + run: | + # The boot partition from this tree's mtdparts: NOR boot = 256 KiB + # (env at 0x40000). An image larger than it overflows into env and + # bricks the device on upgrade. The image includes the loader + # and the device tree it reads. + soc='${{ matrix.soc }}' + rc=0 + check_kind() { + kind=$1 + limit_kb=$2 + pattern=$3 + found=0 + for f in $pattern; do + [ -f "$f" ] || continue + found=1 + # Compare bytes: rounding down to KiB would pass an image up + # to 1023 bytes into the environment. + size=$(stat -c %s "$f") + limit=$((limit_kb * 1024)) + echo "- $f: [${size}/${limit} bytes]" + if [ "$size" -gt "$limit" ]; then + echo "-- size exceeded by: $((size - limit)) bytes" >&2 + rc=1 + fi + done + [ "$found" = 1 ] || { echo "missing ${kind} image for ${soc} (${pattern})" >&2; rc=1; } + } + check_kind nor 256 "output/*${soc}*-nor.bin" + exit $rc + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: u-boot-${{ matrix.soc }} + path: ${{ matrix.files }} + if-no-files-found: error + + # Build qemu-system-arm once and share it with every smoke leg. + qemu: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Install QEMU build deps + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq --no-install-recommends \ + build-essential ninja-build meson pkg-config \ + libglib2.0-dev libpixman-1-dev libfdt-dev zlib1g-dev \ + libslirp-dev python3 python3-venv + + - name: Build qemu-system-arm + run: | + git clone --depth 1 --branch master \ + https://github.com/widgetii/qemu-hisilicon qemu-hisilicon + cd qemu-hisilicon + bash qemu/setup.sh + cp qemu-src/build/qemu-system-arm "$GITHUB_WORKSPACE/qemu-arm" + chmod +x "$GITHUB_WORKSPACE/qemu-arm" + + - name: Upload qemu-arm + uses: actions/upload-artifact@v4 + with: + name: qemu-arm + path: qemu-arm + + # QEMU smoke test -- boots each freshly-built image from flash through + # qemu-hisilicon's nt98562/nt98566 machines, which do the loader's job + # (they do not train DDR, so they do not run it): read the device tree + # at 0x10000, unpack U-Boot from its BCL1 container as the tree says, + # fill SHMINFO and start it; then assert it reaches the autoboot prompt. + # Both SoCs have a machine model, so both are a hard gate on release. + # + # The second boot must load the environment the first saved, which + # proves the driver's writes land where its reads find them. + qemu_smoke: + needs: [build, qemu] + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + soc: [nt98562, nt98566] + steps: + - name: Download qemu-arm + uses: actions/download-artifact@v4 + with: + name: qemu-arm + + - name: Download u-boot artifact + uses: actions/download-artifact@v4 + with: + name: u-boot-${{ matrix.soc }} + + - name: Install QEMU runtime deps + run: | + # qemu-arm is built once in the `qemu` job and downloaded here, + # so this leg needs the shared libraries it links against. + sudo apt-get update -qq + sudo apt-get install -y -qq --no-install-recommends \ + libglib2.0-0 libpixman-1-0 libfdt1 zlib1g libslirp0 + if ldd ./qemu-arm | grep -qi 'not found'; then + echo "qemu-arm has unresolved libraries:" >&2 + ldd ./qemu-arm >&2 + exit 1 + fi + + - name: Smoke-test in QEMU + timeout-minutes: 3 + run: | + soc='${{ matrix.soc }}' + UBOOT="u-boot-${soc}-nor.bin" + test -f "$UBOOT" + echo "booting $UBOOT" + # 8 MiB NOR, U-Boot at 0, the rest erased + python3 - "$UBOOT" /tmp/flash.bin <<'PY' + import sys + img = bytearray(b'\xff' * (8 << 20)) + u = open(sys.argv[1], 'rb').read() + img[:len(u)] = u + open(sys.argv[2], 'wb').write(img) + PY + FLASH="-M ${soc},flash-file=/tmp/flash.bin" + chmod +x ./qemu-arm + # Two boots: the first finds an erased environment and saves the + # default one; the second must load it back. + for boot in 1 2; do + mkfifo /tmp/uart.in /tmp/uart.out + ./qemu-arm $FLASH \ + -nographic -serial pipe:/tmp/uart \ + -monitor none > /tmp/qemu.log 2>&1 & + QEMU_PID=$! + # Wait for the autoboot prompt, not the banner: the banner prints + # before the board init that can hang. Up to 30 s. + cat /tmp/uart.out > /tmp/uboot$boot.txt & + CAT_PID=$! + for i in $(seq 1 60); do + grep -q 'Hit Ctrl-C' /tmp/uboot$boot.txt 2>/dev/null && break + sleep 0.5 + done + kill "$QEMU_PID" "$CAT_PID" 2>/dev/null || true + wait 2>/dev/null || true + rm -f /tmp/uart.in /tmp/uart.out + echo "== boot $boot: captured u-boot output ==" + cat /tmp/uboot$boot.txt + echo + if ! grep -q 'Hit Ctrl-C' /tmp/uboot$boot.txt; then + echo "FAIL: boot $boot did not reach the autoboot prompt" + echo + echo "== qemu stderr ==" + tail -40 /tmp/qemu.log || true + exit 1 + fi + done + if grep -q 'bad CRC' /tmp/uboot2.txt; then + echo "FAIL: the environment saved on the first boot did not land" + exit 1 + fi + echo "PASS: u-boot reached the autoboot prompt, and its saved environment survived a reset" + + # Mirror every built image to this repository's own Novatek release. + release: + needs: [resolve, build, qemu_smoke] + if: ${{ github.event_name == 'workflow_dispatch' && inputs.release }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + pattern: u-boot-* + + - name: Upload to releases + uses: softprops/action-gh-release@v2 + with: + tag_name: novatek + name: Novatek + target_commitish: ${{ needs.resolve.outputs.sha }} + prerelease: true + files: artifacts/**/*.bin + fail_on_unmatched_files: true + + # Publish every image to the OpenIPC/firmware `latest` release and tell + # openipc.org what was uploaded. openipc.org keeps, per file name, the + # newest uboot build that names it, so this run adds or refreshes the + # u-boot-nt985* names and leaves the other repositories' files alone. The + # concurrency group, shared with build-hisilicon.yml, keeps a second run + # from uploading between this run's upload and its report. + publish: + needs: [resolve, build, qemu_smoke] + if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish }} + runs-on: ubuntu-latest + timeout-minutes: 30 + concurrency: + group: publish-firmware-latest + cancel-in-progress: false + permissions: + contents: read + id-token: write + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + pattern: u-boot-* + + - name: Upload to OpenIPC/firmware latest release + env: + GH_TOKEN: ${{ secrets.FIRMWARE_RELEASE_TOKEN }} + run: | + set -eu + # Releases API occasionally returns 502/503. Retry with backoff: + # 10s, 20s, 30s, 40s -- total ~100s before giving up. + upload() { + local f="$1" i + for i in 1 2 3 4 5; do + if gh release upload latest "$f" --clobber -R OpenIPC/firmware; then + return 0 + fi + [ "$i" = "5" ] && return 1 + echo "upload attempt $i failed; sleeping $((i*10))s" >&2 + sleep $((i*10)) + done + } + find artifacts -type f -name '*.bin' | sort | while read -r f; do + echo "publishing $f" + upload "$f" + done + + # The push script is OpenIPC/firmware's, at a fixed commit: the only + # code this job checks out, since it holds an id-token. + - uses: actions/checkout@v4 + with: + repository: OpenIPC/firmware + ref: 7fa63ce694cb5d27c19d4f19058ba2c044849db4 + sparse-checkout: .github/scripts + path: firmware + + # The contract is PUSH.md in OpenIPC/website (source `uboot`, release + # `latest`, one id per run). The digests are of the files uploaded + # above, hashed here, so the push describes exactly those bytes. + - name: Report to openipc.org + env: + NVT_SHA: ${{ needs.resolve.outputs.sha }} + run: | + set -eu + mkdir published + find artifacts -type f -name '*.bin' -exec cp {} published/ \; + python3 firmware/.github/scripts/push_build.py \ + --source uboot \ + --build-id "uboot-novatek-$(date -u +%Y%m%dT%H%M%SZ)-${NVT_SHA::7}" \ + --release latest \ + --sha "$NVT_SHA" \ + --built-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --assets 'published/*.bin' diff --git a/CLAUDE.md b/CLAUDE.md index 3367bdd8dea7..fae44a0dee5c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,11 @@ own branch: CI: `build-anyka.yml`, called from that branch's `ci.yml`. Its own `CLAUDE.md` covers the boot headers, the EV330 first stage and the drivers. +- `openipc-novatek`: U-Boot for Novatek NT98562/NT98566 (NA51089), on + the same mainline release with the same OpenIPC layer, plus the SDK's + loader, built from source in the tree. CI: `build-novatek.yml`, called + from that branch's `ci.yml`. Its own `CLAUDE.md` covers the loader, the + image layout and the kernel hand-off. - `main`: a fork of upstream mainline U-Boot, kept for rebasing. To change bootloader code, check out `openipc-hisilicon`, or read it with @@ -122,6 +127,27 @@ The build logic lives here. Other branches call it: (OpenIPC/website `service/internal/builds/verify.go`). `build-anyka.yml` has to be added there before the first `publish`. +## `build-novatek.yml` + +- It is `build-anyka.yml` for `openipc-novatek`: the same job graph and + `resolve`/`ref` rules, NOR only (256 KiB boot partition: the loader, + the device tree at 0x10000 and U-Boot at 0x20000). +- The build legs also install `gcc-arm-none-eabi`, for the loader. +- **No `hold`:** both SoCs have qemu-hisilicon machines (`nt98562`, + `nt98566`). Their stand-in for the loader must unpack an LZMA BCL1 + container (algorithm 11), which qemu-hisilicon needs for this smoke + test. +- **Smoke test:** two boots of an 8 MiB flash image holding only the + image. It passes on `Hit Ctrl-C`, and fails if the second boot reports + a bad environment CRC. +- **Release and publish:** + - `release` uploads to the `novatek` tag. + - `publish` shares the `publish-firmware-latest` concurrency group and + reports a `uboot-novatek-…` build id. + - openipc.org accepts the report only from workflows it allowlists + (OpenIPC/website `service/internal/builds/verify.go`). + `build-novatek.yml` has to be added there before the first `publish`. + ## Invariants to keep when editing `build-hisilicon.yml` - **Three SoC lists must agree**: the `build` matrix, the `qemu_smoke` matrix, diff --git a/README.md b/README.md index 12f250db8340..24d82245bb0a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![xiongmai build](https://github.com/OpenIPC/u-boot/actions/workflows/build-xm.yml/badge.svg)](https://github.com/OpenIPC/u-boot/actions/workflows/build-xm.yml) [![gk710x build](https://github.com/OpenIPC/u-boot/actions/workflows/build-gk710x.yml/badge.svg)](https://github.com/OpenIPC/u-boot/actions/workflows/build-gk710x.yml) [![anyka build](https://github.com/OpenIPC/u-boot/actions/workflows/build-anyka.yml/badge.svg)](https://github.com/OpenIPC/u-boot/actions/workflows/build-anyka.yml) +[![novatek build](https://github.com/OpenIPC/u-boot/actions/workflows/build-novatek.yml/badge.svg)](https://github.com/OpenIPC/u-boot/actions/workflows/build-novatek.yml) Unified repository for the U-Boot bootloaders maintained by [OpenIPC](https://openipc.org) for supported SoCs. @@ -19,6 +20,7 @@ family — not bootloader sources. | `openipc-xm` | U-Boot for Xiongmai XM510/XM530/XM550 | | `openipc-gk710x` | U-Boot for Goke GK7101/GK7102 | | `openipc-anyka` | U-Boot for Anyka AK3918EV200, AK391xEV300, AK391xEV330 | +| `openipc-novatek` | U-Boot for Novatek NT98562, NT98566 | | `main` | Fork of upstream mainline U-Boot, kept for rebasing | New families are added here as they are ported, each with its own @@ -192,6 +194,44 @@ bash build.sh ak3918ev300 # a single SoC Images land in `output/` as `u-boot--nor.bin`. +## novatek + +U-Boot for Novatek's NA51089 camera SoCs, on the same mainline release +(v2022.10) and with the same OpenIPC boot layer as `openipc-hisilicon`. + +**NOR:** `nt98562` (64 MiB of DDR in the package), `nt98566` (128 MiB). + +The mask ROM starts Novatek's loader, which trains the DDR, reads a device +tree from flash and unpacks U-Boot as that tree says. The loader is built +from the Novatek SDK's sources, in this tree, and `u-boot--nor.bin` +holds all three, to be written at flash offset 0 as it is: + +| Offset | Contents | +| --- | --- | +| `0x00000` | the loader | +| `0x10000` | the device tree, a Xiongmai camera's, which the kernel gets too | +| `0x20000` | U-Boot, LZMA-packed | +| `0x40000` | the environment (not in the image) | + +OpenIPC kernels for these SoCs carry no device tree: U-Boot boots them with +the loader's, giving Linux the first `osmem` of the DDR and the vendor +media modules the rest, as the vendor U-Boot does. + +These images have so far run only on qemu-hisilicon's NT9856x machines, +not on a camera. + +```sh +git clone --branch openipc-novatek https://github.com/OpenIPC/u-boot.git +cd u-boot + +bash build.sh # nt98562 nt98566 +bash build.sh nt98566 # a single SoC +``` + +The loader needs an Arm bare-metal toolchain (`arm-none-eabi-`, else the +one U-Boot uses; `LOADER_CROSS_COMPILE` overrides). Images land in +`output/` as `u-boot--nor.bin`. + ## CI The [build-hisilicon](.github/workflows/build-hisilicon.yml) workflow runs @@ -288,6 +328,25 @@ Its `release` input fills this repository's gh workflow run build-anyka.yml -f release=true -f publish=true ``` +### novatek + +The [build-novatek](.github/workflows/build-novatek.yml) workflow has the +same shape. It runs on pull requests to this branch that change it, on +pushes and pull requests to `openipc-novatek` (through that branch's +`ci.yml`), and manually. Both SoCs have a qemu-hisilicon machine model, +which does the loader's job, so both images are smoke-tested: booted from +an 8 MiB flash image, then a second time, which must load the environment +the first boot saved. + +Its `release` input fills this repository's +[`Novatek` release](../../releases/tag/novatek); `publish` uploads to +`OpenIPC/firmware` `latest`, where no other repository publishes +`u-boot-nt985*` names, and reports to openipc.org. + +```sh +gh workflow run build-novatek.yml -f release=true -f publish=true +``` + ## Adding a SoC family 1. Push the port on a new branch, based on a mainline U-Boot release or From 44a745a5431e1aad2a071df0fe7b96788d5798b2 Mon Sep 17 00:00:00 2001 From: Dmitry Ermakov <7954832+dimerr@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:28:04 +0000 Subject: [PATCH 2/2] docs: the bash /bin/sh and vendor tools are build-hisilicon.yml's CLAUDE.md read as if every family's CI needed /bin/sh as bash and pycryptodome, u-boot-tools and lzop; the Qodo review of #54 asked for them in build-novatek.yml on that basis. They are what the openipc-hisilicon vendor recipes use. openipc-novatek's build.sh runs none of them: the loader's make system and nvt-image.py build both images with /bin/sh as dash (checked locally). --- CLAUDE.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fae44a0dee5c..387dfc9dbd27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -200,9 +200,12 @@ gh workflow run build-hisilicon.yml -f release=true # + own re gh workflow run build-hisilicon.yml -f release=true -f publish=true # + OpenIPC/firmware + openipc.org ``` -CI's build legs symlink `/bin/sh` to bash, because the vendor recipes depend on -bash-isms. They also need `pycryptodome`, `u-boot-tools`, -`device-tree-compiler` and `lzop`. +`build-hisilicon.yml`'s build legs symlink `/bin/sh` to bash, because the +`openipc-hisilicon` vendor recipes depend on bash-isms. They also need +`pycryptodome`, `u-boot-tools`, `device-tree-compiler` and `lzop`. The other +families' builds use none of those recipes: their workflows install what their +`build.sh` runs (for `openipc-novatek`, also `gcc-arm-none-eabi`; its loader's +make system builds under dash). ## Conventions