diff --git a/.github/workflows/build-allwinner.yml b/.github/workflows/build-allwinner.yml new file mode 100644 index 000000000000..c897135f6eb4 --- /dev/null +++ b/.github/workflows/build-allwinner.yml @@ -0,0 +1,378 @@ +name: Build allwinner + +on: + # Pull requests against this branch change the workflow itself; they + # build the current openipc-allwinner. Code changes reach it through + # workflow_call from the caller on openipc-allwinner, which passes the revision + # to build. Only workflow_dispatch can release or publish. + pull_request: + branches: [openipc] + paths: + - .github/workflows/build-allwinner.yml + workflow_call: + inputs: + ref: + description: openipc-allwinner revision to build + type: string + required: true + workflow_dispatch: + inputs: + release: + description: Publish to this repo's Allwinner 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-allwinner 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-allwinner' }} + 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: v831, files: "output/u-boot-v831-*.bin" } + - { soc: v833, files: "output/u-boot-v833-*.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 xz-utils + } + 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 is the SPL (eGON.BT0) + # and the LZMA-packed U-Boot behind it. + 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" + # On a card the image starts at 8 KiB and the first partition + # at 1 MiB, but it must also fit the NOR it is there to flash. + check_kind mmc 256 "output/*${soc}*-mmc.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 v831/v833 machines, which do the boot ROM's job (load + # the eGON.BT0 SPL into SRAM; the SPL brings up the DDR and unpacks + # U-Boot), and asserts it reaches the autoboot prompt. Every SoC has a + # machine model, so every image is a hard gate on release. The -mmc legs + # boot from the SD card, which the machines' boot ROM tries first. + # + # 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: + include: + - { soc: v831, kind: nor } + - { soc: v831, kind: mmc } + - { soc: v833, kind: nor } + - { soc: v833, kind: mmc } + 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 }}' + kind='${{ matrix.kind }}' + UBOOT="u-boot-${soc}-${kind}.bin" + test -f "$UBOOT" + echo "booting $UBOOT" + # nor: 8 MiB NOR, U-Boot at 0, the rest erased. mmc: the NOR + # erased, U-Boot at 8 KiB of a 64 MiB card, which the boot ROM + # tries first; the environment is in the NOR either way. + python3 - "$UBOOT" "$kind" /tmp/flash.bin /tmp/sd.img <<'PY' + import sys + u = open(sys.argv[1], 'rb').read() + img = bytearray(b'\xff' * (8 << 20)) + if sys.argv[2] == 'nor': + img[:len(u)] = u + else: + sd = bytearray(64 << 20) + sd[8192:8192 + len(u)] = u + open(sys.argv[4], 'wb').write(sd) + open(sys.argv[3], 'wb').write(img) + PY + FLASH="-M ${soc},flash-file=/tmp/flash.bin" + [ "$kind" = mmc ] && FLASH="$FLASH -drive if=sd,format=raw,file=/tmp/sd.img" + 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 + # The first boot finds the environment erased and the OpenIPC + # helper saves the default one (no command over the UART needed); + # the second must read it back. + if ! grep -q 'Saving Environment to SPIFlash' /tmp/uboot1.txt; then + echo "FAIL: the first boot did not save the environment" + exit 1 + fi + 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 Allwinner 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: allwinner + name: Allwinner + 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-v831/v833 names and leaves the other repositories' files alone + # (OpenIPC/u-boot-allwinner publishes u-boot-v851s-nor.bin). 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: + AW_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-allwinner-$(date -u +%Y%m%dT%H%M%SZ)-${AW_SHA::7}" \ + --release latest \ + --sha "$AW_SHA" \ + --built-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --assets 'published/*.bin' diff --git a/CLAUDE.md b/CLAUDE.md index 387dfc9dbd27..d21fbecc7473 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,11 @@ own branch: 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. +- `openipc-allwinner`: U-Boot for Allwinner V831/V833 (sun8iw19), on + the same mainline release with the same OpenIPC layer, as a mainline + sunxi SoC with its own SPL and DRAM driver. CI: `build-allwinner.yml`, + called from that branch's `ci.yml`. Its own `CLAUDE.md` covers the SPL, + the DRAM port 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 @@ -148,6 +153,33 @@ The build logic lives here. Other branches call it: (OpenIPC/website `service/internal/builds/verify.go`). `build-novatek.yml` has to be added there before the first `publish`. +## `build-allwinner.yml` + +- It is `build-anyka.yml` for `openipc-allwinner`: the same job graph and + `resolve`/`ref` rules, NOR only (256 KiB boot partition: the SPL, then + U-Boot LZMA-packed at 0x8000). +- The build legs also install `xz-utils`, for the LZMA image. +- **No `hold`:** both SoCs have qemu-hisilicon machines (`v831`, + `v833`). The `-mmc` legs need its boot ROM stand-in to boot from the SD + card (qemu-bench #204). +- **Two kinds per SoC:** `u-boot--nor.bin` and `-mmc.bin` (SD card + boot: the SPL reads U-Boot from the card); one artifact holds both, the + smoke matrix has a leg per kind. +- **Smoke test:** two boots, `-nor` from an 8 MiB flash holding only the + image, `-mmc` from a 64 MiB card with it at 8 KiB and the flash erased. + It passes on `Hit Ctrl-C`, and fails if the second boot reports a bad + environment CRC (the environment is in the flash for both). +- **Release and publish:** + - `release` uploads to the `allwinner` tag. + - `publish` shares the `publish-firmware-latest` concurrency group and + reports a `uboot-allwinner-…` build id. OpenIPC/u-boot-allwinner + publishes `u-boot-v851s-nor.bin` to the same release; the names do + not overlap. + - openipc.org accepts the report only from workflows it allowlists + (OpenIPC/website `service/internal/builds/verify.go`). + `build-allwinner.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 24d82245bb0a..11c684e5fd65 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![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) +[![allwinner build](https://github.com/OpenIPC/u-boot/actions/workflows/build-allwinner.yml/badge.svg)](https://github.com/OpenIPC/u-boot/actions/workflows/build-allwinner.yml) Unified repository for the U-Boot bootloaders maintained by [OpenIPC](https://openipc.org) for supported SoCs. @@ -21,6 +22,7 @@ family — not bootloader sources. | `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 | +| `openipc-allwinner` | U-Boot for Allwinner V831, V833 | | `main` | Fork of upstream mainline U-Boot, kept for rebasing | New families are added here as they are ported, each with its own @@ -232,6 +234,59 @@ 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`. +## allwinner + +U-Boot for the Allwinner V831 and V833 (sun8iw19), on the same mainline +release (v2022.10) and with the same OpenIPC boot layer as +`openipc-hisilicon`, as a mainline sunxi SoC with its own SPL: no vendor +boot0, OP-TEE or boot package. + +**NOR and SD card:** `v831` (64 MiB DDR2 in the package), `v833` +(128 MiB DDR3). + +The boot ROM loads the SPL from flash offset 0 into SRAM. The SPL brings +up the DDR -- the vendor boot0's DRAM library, translated to C -- and +unpacks U-Boot, LZMA-packed, from offset 0x8000. `u-boot--nor.bin` +holds both, ~170 KiB, to be written at flash offset 0 as it is: + +| Offset | Contents | +| --- | --- | +| `0x00000` | the SPL (eGON.BT0) | +| `0x08000` | U-Boot, LZMA-packed | +| `0x40000` | the environment (not in the image) | + +**SD card (unbricking):** `u-boot--mmc.bin` is the same image with +an SPL that loads U-Boot from the card. The boot ROM tries the card +before the flash, so a camera with a broken or empty flash boots from a +card that has it at 8 KiB, before the first partition: + +```sh +dd if=u-boot-v831-mmc.bin of=/dev/sdX bs=1k seek=8 conv=fsync +``` + +Booted that way, U-Boot reads its update files from the card's FAT +partition (`source` is `fatload mmc 0`): `run ub` writes +`u-boot--nor.bin` to the flash, `run fwupd` `rootfs.squashfs.` +and `uImage.`. Remove the card and the camera boots from the flash. + +OpenIPC kernels for these SoCs carry no device tree: U-Boot boots them +with the vendor's (the Tina SDK's reference board), built into U-Boot, +in the normal world with PSCI, as the vendor's OP-TEE and U-Boot do. + +These images have so far run only on qemu-hisilicon's V831/V833 +machines, not on a camera. + +```sh +git clone --branch openipc-allwinner https://github.com/OpenIPC/u-boot.git +cd u-boot + +bash build.sh # v831 v833 +bash build.sh v831 # a single SoC +``` + +The toolchain is `arm-linux-gnueabi-`, and `xz` packs U-Boot. Images land +in `output/` as `u-boot--nor.bin` and `u-boot--mmc.bin`. + ## CI The [build-hisilicon](.github/workflows/build-hisilicon.yml) workflow runs @@ -347,6 +402,25 @@ Its `release` input fills this repository's gh workflow run build-novatek.yml -f release=true -f publish=true ``` +### allwinner + +The [build-allwinner](.github/workflows/build-allwinner.yml) workflow has +the same shape. It runs on pull requests to this branch that change it, +on pushes and pull requests to `openipc-allwinner` (through that branch's +`ci.yml`), and manually. Every SoC has a qemu-hisilicon machine +model, so every image is smoke-tested: booted -- the `-nor` one from an +8 MiB flash, the `-mmc` one from a card with the flash erased -- then a +second time, which must load the environment the first saved. + +Its `release` input will fill this repository's +[`Allwinner` release](../../releases/tag/allwinner); `publish` uploads to +`OpenIPC/firmware` `latest`, where OpenIPC/u-boot-allwinner publishes only +`u-boot-v851s-nor.bin`, and reports to openipc.org. + +```sh +gh workflow run build-allwinner.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