ci: build and test x16emu natively on narvi-1, OxygenHub's RISC-V runner a673804c781c
An OxygenFlow pipeline (.flow/pipeline.kdl) with two jobs on narvi-1, an OrangePi RV2 (Ky X1, riscv64), in the syscall tier since the board has no hardware virtualisation: - build: make as README.md gives it, with the Makefile's -Werror, and the riscv64 binary kept as the artifact x16emu-linux-riscv64 with the runner's signed receipt; - conformance: that binary, taken as the job's input, against the retro-remote-debug-controller conformance suite, headless, the way .github/workflows/conformance.yml runs it on x86 (zero-filled ROM, -warp, a loopback control port); pytest's JUnit XML is kept for the hub's Tests tab, and a run that skipped every test fails. The image is Ubuntu Base 24.04.5 riscv64, checked against its published SHA-256, with build-essential, make, libsdl2-dev, zlib1g-dev, python3 and python3-pytest installed by apt in a chroot (.flow/image/build-riscv64.sh; there is no Docker on the board). It is imported on narvi-1 as cas://blob/a1405289def4850dade956ea6d4e4870a73bb631cc353f16f0e5fcd796fe2541. Importing the same tarball into another Flow home gave another hash, so the pin is the image narvi-1 holds; rebuild, re-import there and re-pin when the package list changes.
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 7f1108192b02
- When
- · 2026-10-21 02:57 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
a673804c781c66835a6e686397bb9cc24ccd999956adef71ea1c37222c4034fc- Depends on
- nothing: a root change
- Depended on by
- 10b68259f181 712ea2b4d346
- In bookmarks
- main riscv-1
- Edits
- 4 atoms, 7 tree operations, 4 files
Browse the tree at this change
added .flow/image/build-riscv64.sh +52 −0
| 1 | + | #!/bin/sh |
|
| 2 | + | # Build the riscv64 image the pipeline's jobs run in on narvi-1, and write it |
|
| 3 | + | # as a root file system tarball for `flow rootfs import`. There is no Docker |
|
| 4 | + | # on a RISC-V board, so this is Ubuntu Base 24.04.5 riscv64, checked against |
|
| 5 | + | # its published SHA-256, with the build's packages installed by apt in a |
|
| 6 | + | # chroot. Run it on a riscv64 Linux machine (narvi) as root or a user with |
|
| 7 | + | # sudo, then |
|
| 8 | + | # |
|
| 9 | + | # FLOW_HOME=~/flow ~/flow/bin/flow rootfs import x16emu-riscv64.tar |
|
| 10 | + | # |
|
| 11 | + | # and pin the printed hash in .flow/pipeline.kdl. Rebuild, re-import and |
|
| 12 | + | # re-pin when the packages below change. |
|
| 13 | + | set -eu |
|
| 14 | + | here=$(cd "$(dirname "$0")" && pwd) |
|
| 15 | + | out=${1:-$here/x16emu-riscv64.tar} |
|
| 16 | + | release=24.04.5 |
|
| 17 | + | base=ubuntu-base-$release-base-riscv64.tar.gz |
|
| 18 | + | sha256=651c516f291259ec913caa1efd0cfff738cd223adfc6617c884f07af487f08cf |
|
| 19 | + | # What the Makefile and the conformance suite need, as README.md lists for |
|
| 20 | + | # riscv64 Linux: C11 and C++17 compilers, make, SDL2 and zlib, Python and |
|
| 21 | + | # pytest. |
|
| 22 | + | packages="build-essential make libsdl2-dev zlib1g-dev python3 python3-pytest" |
|
| 23 | + | ||
| 24 | + | test "$(uname -m)" = riscv64 || { echo "build-riscv64.sh: run this on a riscv64 machine" >&2; exit 1; } |
|
| 25 | + | work=$(mktemp -d) |
|
| 26 | + | root=$work/root |
|
| 27 | + | cleanup() { |
|
| 28 | + | sudo umount "$root/proc" "$root/dev" 2>/dev/null || true |
|
| 29 | + | sudo rm -rf "$work" |
|
| 30 | + | } |
|
| 31 | + | trap cleanup EXIT |
|
| 32 | + | ||
| 33 | + | curl -fsSL -o "$work/$base" "https://cdimage.ubuntu.com/ubuntu-base/releases/$release/release/$base" |
|
| 34 | + | echo "$sha256 $work/$base" | sha256sum -c - >/dev/null |
|
| 35 | + | mkdir "$root" |
|
| 36 | + | sudo tar -C "$root" -xzf "$work/$base" |
|
| 37 | + | sudo mv "$root/etc/resolv.conf" "$work/resolv.conf.image" 2>/dev/null || true |
|
| 38 | + | sudo cp /etc/resolv.conf "$root/etc/resolv.conf" |
|
| 39 | + | sudo mount --bind /dev "$root/dev" |
|
| 40 | + | sudo mount -t proc proc "$root/proc" |
|
| 41 | + | sudo chroot "$root" sh -c " |
|
| 42 | + | export DEBIAN_FRONTEND=noninteractive |
|
| 43 | + | apt-get update -q >/dev/null |
|
| 44 | + | apt-get install -y -q --no-install-recommends $packages >/dev/null |
|
| 45 | + | apt-get clean |
|
| 46 | + | rm -rf /var/lib/apt/lists/*" |
|
| 47 | + | sudo umount "$root/proc" "$root/dev" |
|
| 48 | + | sudo rm "$root/etc/resolv.conf" |
|
| 49 | + | sudo mv "$work/resolv.conf.image" "$root/etc/resolv.conf" 2>/dev/null || true |
|
| 50 | + | sudo tar -C "$root" -cf "$out" . |
|
| 51 | + | sudo chown "$(id -u):$(id -g)" "$out" |
|
| 52 | + | echo "$out" |
added .flow/jobs/build.sh +7 −0
| 1 | + | # x16emu and makecart built natively on narvi-1, a riscv64 board, the way |
|
| 2 | + | # README.md says to build them on riscv64 Linux: plain `make`, with the |
|
| 3 | + | # Makefile's own -O3 -Wall -Werror, so a new warning fails the job. |
|
| 4 | + | set -eu |
|
| 5 | + | echo "building on $(uname -m), $(cc --version | head -1)" |
|
| 6 | + | make -j6 |
|
| 7 | + | ./x16emu -version |
added .flow/jobs/conformance.sh +28 −0
| 1 | + | # The retro-remote-debug-controller conformance suite against the x16emu the |
|
| 2 | + | # build job made on narvi-1, as .github/workflows/conformance.yml runs it on |
|
| 3 | + | # x86: headless (SDL's dummy drivers), -warp, on a loopback port, with a |
|
| 4 | + | # zero-filled ROM, since the suite exercises the control API, not the ROM. |
|
| 5 | + | # pytest's JUnit XML goes to the hub's Tests tab, kept when the job fails too. |
|
| 6 | + | set -eu |
|
| 7 | + | chmod +x x16emu |
|
| 8 | + | truncate -s 262144 dummy-rom.bin |
|
| 9 | + | export RRDC_PORT=8080 |
|
| 10 | + | export RRDC_BOOT_TIMEOUT=60 |
|
| 11 | + | export RRDC_EMU_CMD="SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy $PWD/x16emu -rom $PWD/dummy-rom.bin -warp -controlport {port}" |
|
| 12 | + | status=0 |
|
| 13 | + | (cd src/extern/retro-remote-debug-controller/conformance && |
|
| 14 | + | python3 -m pytest -v -rs -p no:cacheprovider --junitxml=/src/junit.xml) || status=$? |
|
| 15 | + | # A suite that skipped every test never reached the emulator: that is a |
|
| 16 | + | # failure here, where one is always there to reach. |
|
| 17 | + | python3 - <<'PY' |
|
| 18 | + | import sys |
|
| 19 | + | import xml.etree.ElementTree as ET |
|
| 20 | + | root = ET.parse("/src/junit.xml").getroot() |
|
| 21 | + | suites = [root] if root.tag == "testsuite" else list(root) |
|
| 22 | + | tests = sum(int(s.get("tests", 0)) for s in suites) |
|
| 23 | + | skipped = sum(int(s.get("skipped", 0)) for s in suites) |
|
| 24 | + | print(f"conformance: {tests} tests, {skipped} skipped") |
|
| 25 | + | if tests == 0 or skipped == tests: |
|
| 26 | + | sys.exit("conformance: every test skipped: the emulator did not answer on its control port") |
|
| 27 | + | PY |
|
| 28 | + | exit $status |
added .flow/pipeline.kdl +51 −0
| 1 | + | // x16emu's pipeline on OxygenFlow: every push to a bookmark on OxygenHub |
|
| 2 | + | // builds the emulator natively on narvi-1, a real riscv64 board, and runs |
|
| 3 | + | // the control-API conformance suite against what it built. The jobs appear |
|
| 4 | + | // as checks on the change and in the Flow tab. The retro-remote-debug- |
|
| 5 | + | // controller subrepo (src/extern) is put in the workspace by the runner. |
|
| 6 | + | schema-version 1 |
|
| 7 | + | ||
| 8 | + | // Ubuntu 24.04 riscv64 with gcc, make, SDL2, zlib, Python and pytest |
|
| 9 | + | // (.flow/image/build-riscv64.sh), in the syscall tier: narvi's Ky X1 has no |
|
| 10 | + | // hardware virtualisation, so no microVM. `runs-on` keeps it on a RISC-V |
|
| 11 | + | // runner. Loopback only: the suite talks to the emulator on 127.0.0.1. |
|
| 12 | + | sandbox "x16-riscv64" { |
|
| 13 | + | runtime "syscall" |
|
| 14 | + | rootfs "cas://blob/a1405289def4850dade956ea6d4e4870a73bb631cc353f16f0e5fcd796fe2541" |
|
| 15 | + | runs-on "riscv64" |
|
| 16 | + | memory-limit "2GiB" |
|
| 17 | + | cpu-limit 6 |
|
| 18 | + | network "loopback" |
|
| 19 | + | } |
|
| 20 | + | ||
| 21 | + | // make as README.md gives it, -Werror included; the binary is kept with the |
|
| 22 | + | // runner's signed receipt and handed to the conformance job. |
|
| 23 | + | job "build" { |
|
| 24 | + | sandbox "x16-riscv64" |
|
| 25 | + | timeout "30m" |
|
| 26 | + | exec "sh" "-eu" ".flow/jobs/build.sh" |
|
| 27 | + | output { |
|
| 28 | + | artifact "x16emu-linux-riscv64" { |
|
| 29 | + | path "/src/x16emu" |
|
| 30 | + | } |
|
| 31 | + | } |
|
| 32 | + | } |
|
| 33 | + | ||
| 34 | + | // The conformance suite (src/extern/retro-remote-debug-controller) against |
|
| 35 | + | // that binary, headless; pytest's JUnit XML for the hub's Tests tab. |
|
| 36 | + | job "conformance" { |
|
| 37 | + | sandbox "x16-riscv64" |
|
| 38 | + | depends-on "build" |
|
| 39 | + | timeout "15m" |
|
| 40 | + | input { |
|
| 41 | + | artifact "x16emu-linux-riscv64" from "build" |
|
| 42 | + | path "/src/x16emu" |
|
| 43 | + | } |
|
| 44 | + | exec "sh" "-eu" ".flow/jobs/conformance.sh" |
|
| 45 | + | output { |
|
| 46 | + | artifact "junit" { |
|
| 47 | + | path "/src/junit.xml" |
|
| 48 | + | when "always" |
|
| 49 | + | } |
|
| 50 | + | } |
|
| 51 | + | } |