From 3ce54fe695247c4e246fdd94d354f7f94b670546 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:40:28 +0300 Subject: [PATCH 1/2] ci: build, smoke-test and publish the Xiongmai openipc-xm images build-xm.yml has build-hisilicon.yml's shape: resolve the openipc-xm revision once, build xm510/xm530/xm550 one per leg with the 256 KiB NOR fit check, build qemu-hisilicon once, smoke-test every image, then release (own `xiongmai` tag) and publish (OpenIPC/firmware latest plus the openipc.org report) on dispatch only. Every SoC has a qemu-hisilicon machine, so none is held. The smoke test boots each NOR image in place from an 8 MiB flash image twice, with the part powering up under the per-block protection Xiongmai bootloaders leave on (xm-sfc.wps): it passes on the autoboot prompt, and fails if the second boot finds a bad environment CRC -- the first boot's saveenv dropped because the driver did not clear the protection. Checked locally with the smoke script as written, against qemu-hisilicon with #150. The pull_request trigger is limited to changes to this file, so other openipc pull requests do not start the xiongmai matrix. publish shares build-hisilicon's concurrency group; openipc.org keeps the newest uboot build per file name, so the two workflows' pushes do not displace each other's files. README and CLAUDE.md describe the openipc-xm branch and the workflow. --- .github/workflows/build-xm.yml | 352 +++++++++++++++++++++++++++++++++ CLAUDE.md | 24 +++ README.md | 47 +++++ 3 files changed, 423 insertions(+) create mode 100644 .github/workflows/build-xm.yml diff --git a/.github/workflows/build-xm.yml b/.github/workflows/build-xm.yml new file mode 100644 index 000000000000..000cecfb8c48 --- /dev/null +++ b/.github/workflows/build-xm.yml @@ -0,0 +1,352 @@ +name: Build xiongmai + +on: + # Pull requests against this branch change the workflow itself; they + # build the current openipc-xm. Code changes reach it through + # workflow_call from the caller on openipc-xm, which passes the revision + # to build. Only workflow_dispatch can release or publish. + pull_request: + branches: [openipc] + paths: + - .github/workflows/build-xm.yml + workflow_call: + inputs: + ref: + description: openipc-xm revision to build + type: string + required: true + workflow_dispatch: + inputs: + release: + description: Publish to this repo's Xiongmai 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-xm 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-xm' }} + 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: xm510, kinds: "nor", files: "output/u-boot-xm510-*.bin" } + - { soc: xm530, kinds: "nor", files: "output/u-boot-xm530-*.bin" } + - { soc: xm550, kinds: "nor", files: "output/u-boot-xm550-*.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 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: | + # Boot partition sizes from this tree's mtdparts: NOR boot = 256 KiB + # (env at 0x40000), NAND/UBI boot = 768 KiB (0xc0000). An image + # larger than its boot partition overflows into env and bricks the + # device on upgrade. + soc='${{ matrix.soc }}' + kinds='${{ matrix.kinds }}' + rc=0 + check_kind() { + kind=$1 + limit_kb=$2 + pattern=$3 + found=0 + for f in $pattern; do + [ -f "$f" ] || continue + found=1 + size_kb=$(( $(stat -c %s "$f") / 1024 )) + echo "- $f: [${size_kb}KB/${limit_kb}KB]" + if [ "$size_kb" -gt "$limit_kb" ]; then + echo "-- size exceeded by: $((size_kb - limit_kb))KB" >&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" + case "$kinds" in + *nand*) check_kind nand 768 "output/*${soc}*-nand.bin" ;; + esac + 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 NOR U-Boot in place from the + # flash, as the boot ROM does (the qemu-hisilicon xiongmai machines start + # at the flash window), and asserts it reaches the autoboot prompt. Every + # SoC has a machine model, so every SoC is a hard gate on release. + # + # The part powers up with SR3.WPS set (xm-sfc.wps), as Xiongmai + # bootloaders leave it: the first boot then also proves that the + # environment save at the end of it lands, i.e. that the flash driver + # clears the per-block protection. + qemu_smoke: + needs: [build, qemu] + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + soc: [xm510, xm530, xm550] + 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 + tr '\0' '\377' < /dev/zero | head -c 8388608 > /tmp/flash.bin + dd if="$UBOOT" of=/tmp/flash.bin bs=1 conv=notrunc 2>/dev/null + 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 \ + -M ${soc},flash-file=/tmp/flash.bin \ + -global xm-sfc.wps=on \ + -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 Xiongmai 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: xiongmai + name: Xiongmai + 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-xm* 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: + XM_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-xm-$(date -u +%Y%m%dT%H%M%SZ)-${XM_SHA::7}" \ + --release latest \ + --sha "$XM_SHA" \ + --built-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --assets 'published/*.bin' diff --git a/CLAUDE.md b/CLAUDE.md index 910f31defac7..774006b31b7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,9 @@ own branch: - `openipc-hisilicon`: U-Boot for HiSilicon/Goke camera SoCs. It has `build.sh`, `openipc/`, `product/` and the vendor tree. +- `openipc-xm`: U-Boot for Xiongmai XM510/XM530/XM550, on the same mainline + release with the same OpenIPC layer. CI: `build-xm.yml`, called from that + branch's `ci.yml`. Its own `CLAUDE.md` covers the run-in-place boot. - `main`: a fork of upstream mainline U-Boot, kept for rebasing. To change bootloader code, check out `openipc-hisilicon`, or read it with @@ -44,6 +47,27 @@ The build logic lives here. Other branches call it: `qemu-system-arm` from widgetii/qemu-hisilicon once) → `qemu_smoke` (hard gate) → `release` / `publish`. These last two run only on `workflow_dispatch`. +## `build-xm.yml` + +- It has the same job graph as `build-hisilicon.yml`, with the same + `resolve`/`ref` rules and the same partition limits. +- `pull_request` runs only when the workflow file itself changes. +- **No `hold`:** every SoC has a qemu-hisilicon machine, so every image is + smoke-tested and released. +- **Smoke test:** two boots of an 8 MiB flash image under + `-global xm-sfc.wps=on`. + - It passes on `Hit Ctrl-C`. + - It fails if the second boot reports a bad environment CRC. That means the + driver did not clear the Xiongmai per-block protection, and the first + boot's `saveenv` was dropped. + - It needs a qemu-hisilicon with the `wps` property (PR #150 or later). +- **Release and publish:** + - `release` uploads to the `xiongmai` tag. + - `publish` shares the `publish-firmware-latest` concurrency group with + hisilicon and reports `--source uboot` under a `uboot-xm-…` build id. + - openipc.org keeps the newest build per file name, so the two workflows' + pushes do not displace each other. + ## 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 fccbe3f4002b..f5a202a05893 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # OpenIPC U-Boot [![hisilicon build](https://github.com/OpenIPC/u-boot/actions/workflows/build-hisilicon.yml/badge.svg)](https://github.com/OpenIPC/u-boot/actions/workflows/build-hisilicon.yml) +[![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) Unified repository for the U-Boot bootloaders maintained by [OpenIPC](https://openipc.org) for supported SoCs. @@ -13,6 +14,7 @@ family — not bootloader sources. | --- | --- | | `openipc` (this branch) | Unified documentation and CI workflows | | `openipc-hisilicon` | U-Boot for HiSilicon/Goke IP-camera SoCs | +| `openipc-xm` | U-Boot for Xiongmai XM510/XM530/XM550 | | `main` | Fork of upstream mainline U-Boot, kept for rebasing | New families are added here as they are ported, each with its own @@ -55,6 +57,29 @@ The toolchain is `arm-linux-gnueabi-` (Debian/Ubuntu package `gcc-arm-linux-gnueabi`). Images land in `output/` as `u-boot--nor.bin` / `u-boot--nand.bin`. +## xiongmai + +U-Boot for Xiongmai's own camera SoCs, on the same mainline release +(v2022.10) and with the same OpenIPC boot layer as `openipc-hisilicon`. + +**NOR:** `xm510`, `xm530`, `xm550` + +The boot ROM runs U-Boot in place from SPI NOR, and U-Boot brings up DDR +itself, so the image is `u-boot.bin` as linked: `u-boot--nor.bin`, +written at flash offset 0. The flash driver clears the per-block write +protection that Xiongmai bootloaders leave on (SR3.WPS), under which the +vendor U-Boot's `sf erase`/`sf write` report success and change nothing. + +```sh +git clone --branch openipc-xm https://github.com/OpenIPC/u-boot.git +cd u-boot + +bash build.sh # xm510 xm530 xm550 +bash build.sh xm530 # a single SoC +``` + +The toolchain is `arm-linux-gnueabi-`, as for hisilicon. + ## CI The [build-hisilicon](.github/workflows/build-hisilicon.yml) workflow runs @@ -90,6 +115,28 @@ gh workflow run build-hisilicon.yml -f release=true # ... and gh workflow run build-hisilicon.yml -f release=true -f publish=true # ... and OpenIPC/firmware + openipc.org ``` +### xiongmai + +The [build-xm](.github/workflows/build-xm.yml) workflow has the same +shape. It runs on pull requests to this branch that change it, on pushes +and pull requests to `openipc-xm` (through that branch's `ci.yml`), and +manually. Every SoC has a qemu-hisilicon machine model, so every image is +smoke-tested: + +- booted in place from an 8 MiB flash image whose part powers up with the + Xiongmai per-block protection on; +- booted a second time, which must load the environment the first boot + saved. + +Its `release` input fills this repository's +[`Xiongmai` release](../../releases/tag/xiongmai); `publish` uploads to +`OpenIPC/firmware` `latest`, where no other repository publishes +`u-boot-xm*` names, and reports to openipc.org. + +```sh +gh workflow run build-xm.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 5379fdfcb9be4f86140ff9b579258f1f632b6c4c Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:19:35 +0300 Subject: [PATCH 2/2] ci: build-xm review follow-ups: exact fit check, no SIGPIPE-prone flash fill - The partition-fit check compares bytes: dividing by 1024 first let an image up to 1023 bytes past 256 KiB through, into the environment. - The erased 8 MiB flash image comes from a finite dd | tr, not an infinite tr cut off by head, which dies of SIGPIPE under pipefail. --- .github/workflows/build-xm.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-xm.yml b/.github/workflows/build-xm.yml index 000cecfb8c48..1477698ada57 100644 --- a/.github/workflows/build-xm.yml +++ b/.github/workflows/build-xm.yml @@ -112,10 +112,13 @@ jobs: for f in $pattern; do [ -f "$f" ] || continue found=1 - size_kb=$(( $(stat -c %s "$f") / 1024 )) - echo "- $f: [${size_kb}KB/${limit_kb}KB]" - if [ "$size_kb" -gt "$limit_kb" ]; then - echo "-- size exceeded by: $((size_kb - limit_kb))KB" >&2 + # 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 @@ -211,7 +214,7 @@ jobs: test -f "$UBOOT" echo "booting $UBOOT" # 8 MiB NOR, U-Boot at 0, the rest erased - tr '\0' '\377' < /dev/zero | head -c 8388608 > /tmp/flash.bin + dd if=/dev/zero bs=1M count=8 2>/dev/null | tr '\0' '\377' > /tmp/flash.bin dd if="$UBOOT" of=/tmp/flash.bin bs=1 conv=notrunc 2>/dev/null chmod +x ./qemu-arm # Two boots: the first finds an erased environment and saves the