OXYGENHUB

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 +
}