From 6e5ec9b84ff39bfdbebffd3b46471352e5eb9122 Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 04:53:31 +0200 Subject: [PATCH 01/10] docs(kernel): Update README with module selection details Expand documentation on selftest.sh behavior, module matching, and kernel compatibility. Clarify the auto-selection process based on `vermagic` and provide examples to guide users through fallback handling and manual overrides. --- kernel/README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/kernel/README.md b/kernel/README.md index 0eece06..38d673a 100644 --- a/kernel/README.md +++ b/kernel/README.md @@ -20,14 +20,35 @@ before connecting the real bus (see PLAN.md §10). ### Running it `selftest.sh` is non-persistent (nothing is installed into `/lib/modules`, no -autoload): it checks vermagic, loads the module, runs the self-test, and always -unloads it again. +autoload): it detects the running kernel, picks the matching module, loads it, +runs the self-test, and always unloads it again. + +A released package ships several kernel builds side by side: + +``` +comodore-iec-emu/ +├── selftest.sh +└── modules/ + ├── iec_listener_1-6.12.93-1+rpt1.ko # built for 6.12.x (bookworm) + └── iec_listener_1-6.18.34-1+rpt1.ko # built for 6.18.x (trixie) +``` + +You don't pick the file yourself: the script reads `uname -r`, then scans +`modules/` (and a few fallback locations) and selects the `.ko` whose **vermagic** +matches the running kernel. Matching is by vermagic rather than filename because +the packaged name carries a Debian epoch/revision (`1:6.12.93-1+rpt1`) that +`uname -r` (`6.12.93+rpt-rpi-v8`) does not. ```bash -sudo ./selftest.sh # auto-finds ./ or ~/iec_listener.ko, address 4 -sudo ./selftest.sh ~/iec_listener.ko --address 5 # runs with ~/iec_listener.ko, address 5 +sudo ./selftest.sh # auto-selects the matching module, address 4 +sudo ./selftest.sh --address 5 # same, address 5 +sudo ./selftest.sh ~/iec_listener.ko --address 5 # force a specific module file ``` +If no module matches the running kernel, the script lists the modules it found +(with the kernel each was built for) and exits — rebuild for the current kernel, +or pass a path explicitly. + It exits non-zero unless the result is a full pass (`0x1F`), so it is usable in scripts/CI. From 2aa51048123d4e61c30560f96ebf92646b1fe927 Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 04:53:40 +0200 Subject: [PATCH 02/10] feat(kernel): Improve selftest.sh module discovery and error handling Enhance module selection process by validating vermagic against the running kernel. Add detailed error messages with suggestions for manual path overrides or rebuilding for the current kernel. Ensure robust fallback behavior and improve user guidance. --- kernel/selftest.sh | 55 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/kernel/selftest.sh b/kernel/selftest.sh index ed4b336..9327686 100755 --- a/kernel/selftest.sh +++ b/kernel/selftest.sh @@ -62,13 +62,60 @@ done case "$ADDRESS" in ''|*[!0-9]*) die "address must be 0-30 (got '$ADDRESS')" ;; esac { [ "$ADDRESS" -ge 0 ] && [ "$ADDRESS" -le 30 ]; } || die "address out of range 0-30: $ADDRESS" +command -v modinfo >/dev/null || die "modinfo not found (install kmod)" + +# ---- locate the module matching the running kernel ---------------------- +# A released package ships one module per supported kernel under modules/. +# The build workflow names each file iec_listener_.ko, with +# ':' and '/' in the version replaced by '-' (1:6.12.93-1+rpt1 -> +# iec_listener_1-6.12.93-1+rpt1.ko); local builds produce a bare +# iec_listener.ko. The glob below ($MODULE*.ko) matches both. +# +# We select by vermagic, not by filename: the package name carries a Debian +# epoch/revision (1:6.12.93-1+rpt1) that `uname -r` (6.12.93+rpt-rpi-v8) does +# not, so the .ko whose vermagic matches the running kernel is the one to load. +RUNNING="$(uname -r)" + +# Where to look, de-duplicated ($(dirname "$0") is often "." when run locally). +SEARCH_DIRS=() +for d in "$(dirname "$0")/modules" "$(dirname "$0")" "./modules" "." "$HOME"; do + skip=0 + for s in "${SEARCH_DIRS[@]:-}"; do [ "$s" = "$d" ] && skip=1; done + [ "$skip" = 0 ] && SEARCH_DIRS+=("$d") +done + if [ -z "$KO" ]; then - for cand in "./${MODULE}.ko" "$HOME/${MODULE}.ko" "$(dirname "$0")/${MODULE}.ko"; do - [ -f "$cand" ] && { KO="$cand"; break; } + info "running kernel: $RUNNING" + for dir in "${SEARCH_DIRS[@]}"; do + [ -d "$dir" ] || continue + for cand in "$dir"/${MODULE}*.ko; do + [ -f "$cand" ] || continue + vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" || continue + if [ "${vm%% *}" = "$RUNNING" ]; then + KO="$cand"; break 2 + fi + done done fi -[ -n "$KO" ] && [ -f "$KO" ] || die "module not found; pass the path, e.g. sudo $0 ~/${MODULE}.ko" -command -v modinfo >/dev/null || die "modinfo not found (install kmod)" + +if [ -z "$KO" ]; then + msg="no module matching the running kernel ($RUNNING) found. + Pass a path explicitly (sudo $0 path/to/${MODULE}.ko), or rebuild for + this kernel. Modules seen:" + any=0 + for dir in "${SEARCH_DIRS[@]}"; do + [ -d "$dir" ] || continue + for cand in "$dir"/${MODULE}*.ko; do + [ -f "$cand" ] || continue + vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" + msg="$msg"$'\n'" $cand (built for ${vm%% *})" + any=1 + done + done + [ "$any" = 1 ] || msg="$msg"$'\n'" (none)" + die "$msg" +fi +[ -f "$KO" ] || die "module not found: $KO" info "module: $KO" info "address: $ADDRESS" From f91b06da9d7585bfb6263143149488610743f42a Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 04:53:45 +0200 Subject: [PATCH 03/10] fix(kernel): Use per-version tags in Docker build to isolate jobs Prevent race conditions in concurrent job builds by introducing unique Docker image tags per kernel version. This ensures isolated builds with correct verm --- .gitea/workflows/build-kernel.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build-kernel.yml b/.gitea/workflows/build-kernel.yml index 7add102..09c47fd 100644 --- a/.gitea/workflows/build-kernel.yml +++ b/.gitea/workflows/build-kernel.yml @@ -51,23 +51,31 @@ jobs: if [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]; then docker run --privileged --rm tonistiigi/binfmt --install arm64 fi + # Per-version image tag. The matrix entries can run concurrently on a + # single runner sharing one Docker daemon; a fixed tag (e.g. iec-kbuild) + # is then a shared mutable name and the builds race — whichever `docker + # build` finishes last wins the tag, so both `docker create` calls + # resolve to the same image and every job emits the same vermagic. + # A unique tag per kernel version isolates them. + img="iec-kbuild:${KERNEL_VERSION//[:\/]/-}" # Builder image: toolchain + matching raspberrypi kernel headers. docker build --platform linux/arm64 \ --build-arg DEBIAN_SUITE="$DEBIAN_SUITE" \ --build-arg HEADERS_PKG="$HEADERS_PKG" \ --build-arg KERNEL_VERSION="$KERNEL_VERSION" \ - -t iec-kbuild . + -t "$img" . # Compile inside the container. We use `docker cp` instead of the bind # mount that build-in-docker.sh uses for local builds: under the runner's # docker-in-docker, /workspace is a volume the host daemon can't see, so # `-v "$PWD:/build"` mounts an empty dir and make finds no Makefile. - cid=$(docker create --platform linux/arm64 --entrypoint sleep iec-kbuild infinity) + cid=$(docker create --platform linux/arm64 --entrypoint sleep "$img" infinity) docker start "$cid" docker cp ./. "$cid:/build" docker exec "$cid" /usr/local/bin/docker-entrypoint.sh clean docker exec "$cid" /usr/local/bin/docker-entrypoint.sh docker cp "$cid:/build/iec_listener.ko" ./iec_listener.ko docker rm -f "$cid" + docker rmi "$img" || true - name: Stage build output (modules/iec_listener_.ko) working-directory: kernel From 2881fae5bfeabbbe67b3afa88c15d2496546ec67 Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 04:57:26 +0200 Subject: [PATCH 04/10] fix(kernel): Correct Docker tag sanitization for kernel versions Ensure Docker image tags map unsupported characters `:`, `/`, and `+` to `-` for compatibility. This prevents invalid tags in Docker builds when kernel version strings include these characters. --- .gitea/workflows/build-kernel.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build-kernel.yml b/.gitea/workflows/build-kernel.yml index 09c47fd..18edaa5 100644 --- a/.gitea/workflows/build-kernel.yml +++ b/.gitea/workflows/build-kernel.yml @@ -56,8 +56,10 @@ jobs: # is then a shared mutable name and the builds race — whichever `docker # build` finishes last wins the tag, so both `docker create` calls # resolve to the same image and every job emits the same vermagic. - # A unique tag per kernel version isolates them. - img="iec-kbuild:${KERNEL_VERSION//[:\/]/-}" + # A unique tag per kernel version isolates them. Docker tags allow only + # [a-zA-Z0-9._-], so map ':' '/' '+' (all present in 1:6.12.93-1+rpt1) + # to '-'; the tag just needs to be unique and valid, not reversible. + img="iec-kbuild:${KERNEL_VERSION//[:\/+]/-}" # Builder image: toolchain + matching raspberrypi kernel headers. docker build --platform linux/arm64 \ --build-arg DEBIAN_SUITE="$DEBIAN_SUITE" \ From 3135a2502bd03f1838ad42ed7b9828a68ab6a28b Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 05:13:48 +0200 Subject: [PATCH 05/10] fix(kernel): Handle unreadable module files in selftest.sh Improve error handling in module discovery by identifying unreadable `.ko` files and providing detailed error messages. Suggest actions like unzip or rebuild to resolve issues. --- kernel/selftest.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/kernel/selftest.sh b/kernel/selftest.sh index 9327686..1b39b7a 100755 --- a/kernel/selftest.sh +++ b/kernel/selftest.sh @@ -107,8 +107,14 @@ if [ -z "$KO" ]; then [ -d "$dir" ] || continue for cand in "$dir"/${MODULE}*.ko; do [ -f "$cand" ] || continue - vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" - msg="$msg"$'\n'" $cand (built for ${vm%% *})" + if vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" && [ -n "$vm" ]; then + note="built for ${vm%% *}" + else + # modinfo couldn't read it - usually not an ELF .ko at all + # (e.g. a still-zipped release file). Show what it really is. + note="UNREADABLE: $(file -b "$cand" 2>/dev/null || echo 'not a kernel module') - unzip/rebuild?" + fi + msg="$msg"$'\n'" $cand ($note)" any=1 done done From 65ff323d504c77b64bf52c86585680bd0d62862a Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 05:29:32 +0200 Subject: [PATCH 06/10] feat(kernel): Include helper scripts and Python frontend in releases Add `launch.sh` and `iecpoc` Python frontend to the release package for streamlined execution on the target device. Ensure accompanying files like `pyproject.toml` and `README.md` are included, while cleaning unnecessary caches. --- .gitea/workflows/build-kernel.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/build-kernel.yml b/.gitea/workflows/build-kernel.yml index 18edaa5..809a2f6 100644 --- a/.gitea/workflows/build-kernel.yml +++ b/.gitea/workflows/build-kernel.yml @@ -100,8 +100,10 @@ jobs: if-no-files-found: error # Release flow only: merges all built modules into one modules/ folder, wraps it - # in a single repo-named top folder alongside selftest.sh, zips it and attaches - # it to the Gitea release for the pushed tag. Skipped entirely on pull requests. + # in a single repo-named top folder alongside the helper scripts (selftest.sh, + # launch.sh) and the iecpoc Python frontend (so launch.sh can pip-install it on + # the Pi), zips it and attaches it to the Gitea release for the pushed tag. + # Skipped entirely on pull requests. release: needs: build if: startsWith(github.ref, 'refs/tags/') @@ -123,7 +125,14 @@ jobs: root="package/$repo" mkdir -p "$root" cp -r dist/modules "$root"/ # modules/iec_listener_.ko - cp kernel/selftest.sh "$root"/ # selftest.sh only at the package root + cp kernel/selftest.sh "$root"/ # one-shot hardware self-test + cp launch.sh "$root"/ # load module -> run frontend -> unload + # iecpoc Python frontend + its packaging metadata. launch.sh pip-installs + # this on the Pi; the build reads pyproject.toml's readme = "README.md", + # so README.md must travel with it. __pycache__ is stripped to stay clean. + cp pyproject.toml README.md "$root"/ + cp -r iecpoc "$root"/ + find "$root/iecpoc" -name __pycache__ -type d -prune -exec rm -rf {} + (cd package && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo") - name: Publish to Gitea release From b3d63664a00c53c57b1f872d7bf155e4a2c84a9a Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 05:31:11 +0200 Subject: [PATCH 07/10] docs(readme): Add instructions for running frontend on Pi Document the process for setting up and running the Python frontend on a Raspberry Pi using release packages. Include details on wiring self-tests, module selection, and using `launch.sh` for streamlined execution. Update kernel README to cross-reference top-level changes. --- README.md | 33 +++++++++++++++++++++++++++++++++ kernel/README.md | 14 ++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 01fae8a..dff03c0 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,39 @@ See [`docs/wiring.md`](docs/wiring.md) for the level-shifter circuit and the pre-C64 bring-up checklist, and [`docs/kernel-notes.md`](docs/kernel-notes.md) for the resolved kernel real-time decisions. +## Running the frontend on the Pi + +A tagged build publishes a release zip that bundles everything needed to run on +a Pi without building anything: + +``` +comodore-iec-emu/ + selftest.sh one-shot wiring self-test (no C64 connected) + launch.sh load module → run iecpoc → unload + pyproject.toml iecpoc packaging metadata + README.md + iecpoc/ the Python frontend + modules/ iec_listener_.ko, one per supported kernel +``` + +Unzip it on the Pi and run **`launch.sh`**. It installs the frontend into a local +`.venv`, detects the running kernel and picks the matching `.ko` (by *vermagic*, +not filename), `insmod`s it, runs `iecpoc`, and **always `rmmod`s on exit** — so +the Pi is left exactly as before: + +```bash +unzip comodore-iec-emu-*.zip && cd comodore-iec-emu +sudo ./selftest.sh # first: check the wiring (want 0x1F) +sudo ./launch.sh # then: load + run the frontend (address 4) +sudo ./launch.sh --address 8 # listen as device 8 +sudo ./launch.sh -- --raw # forward extra args (after --) to iecpoc +``` + +The module is picked automatically; pass `--ko PATH` only to force a specific +file. `launch.sh` needs `python3` plus `python3-venv` (and network on first run +for the build backend). See [`kernel/README.md`](kernel/README.md) for the +self-test details and the per-kernel module selection. + ## Status - **Phase 0 (userspace + tests):** complete, runs on any host. diff --git a/kernel/README.md b/kernel/README.md index 38d673a..97c7912 100644 --- a/kernel/README.md +++ b/kernel/README.md @@ -23,16 +23,26 @@ before connecting the real bus (see PLAN.md §10). autoload): it detects the running kernel, picks the matching module, loads it, runs the self-test, and always unloads it again. -A released package ships several kernel builds side by side: +A released package ships several kernel builds side by side, plus the launcher +and the Python frontend: ``` comodore-iec-emu/ -├── selftest.sh +├── selftest.sh # one-shot wiring self-test (this document) +├── launch.sh # load module → run the iecpoc frontend → unload +├── pyproject.toml # iecpoc packaging metadata (launch.sh pip-installs it) +├── README.md +├── iecpoc/ # the Python userspace decoder/trace └── modules/ ├── iec_listener_1-6.12.93-1+rpt1.ko # built for 6.12.x (bookworm) └── iec_listener_1-6.18.34-1+rpt1.ko # built for 6.18.x (trixie) ``` +`selftest.sh` only verifies the wiring (it loads and immediately unloads). To +actually capture and decode C64 traffic, use **`launch.sh`**, which installs the +frontend, loads the matching module, runs `iecpoc`, and unloads on exit — see the +[top-level README](../README.md#running-the-frontend-on-the-pi). + You don't pick the file yourself: the script reads `uname -r`, then scans `modules/` (and a few fallback locations) and selects the `.ko` whose **vermagic** matches the running kernel. Matching is by vermagic rather than filename because From dafcdfbd7b29ad05b4cabb69fa9efd723bbf0c3b Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 05:31:21 +0200 Subject: [PATCH 08/10] feat(kernel): Add `launch.sh` for streamlined module loading and frontend execution Introduce `launch.sh` as a single execution entry point for the IEC listener module and Python frontend. Automates kernel module selection, virtual environment setup, and cleanup to simplify usage. Ensures consistency by unloading modules on script exit. --- launch.sh | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100755 launch.sh diff --git a/launch.sh b/launch.sh new file mode 100755 index 0000000..2780313 --- /dev/null +++ b/launch.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# +# launch.sh - load the IEC listener module, run the userspace frontend, unload. +# +# This is the "run the whole thing" entry point for a released package. Unlike +# selftest.sh (a one-shot wiring check that loads and immediately unloads), this +# keeps the module loaded for as long as the frontend runs. In order it: +# 1. installs the iecpoc Python frontend into a local virtualenv (.venv), +# 2. detects the running kernel and picks the matching iec_listener_*.ko, +# 3. insmods it (creating /dev/iec0), +# 4. runs `iecpoc`, streaming the decoded C64 traffic to the terminal, +# 5. on exit (Ctrl-C / EOF / error) ALWAYS rmmods again - the Pi is left +# exactly as before. +# +# The module is never left loaded by this script; the EXIT trap unloads it even +# if the frontend crashes or you interrupt it. +# +# Usage: +# sudo ./launch.sh [--address N] [--device PATH] [--ko FILE] [-- ] +# +# Examples: +# sudo ./launch.sh # address 4, /dev/iec0, annotated trace +# sudo ./launch.sh --address 8 # listen as device 8 +# sudo ./launch.sh -- --raw # forward --raw to iecpoc +# sudo ./launch.sh --ko modules/iec_listener_1-6.12.93-1+rpt1.ko +# +# Defaults: address=4 (printer), device=/dev/iec0. + +set -euo pipefail + +MODULE="iec_listener" +ADDRESS=4 +DEVICE="/dev/iec0" +KO="" +VENV=".venv" +LOADED=0 +declare -a PASS=() + +# Run relative to the script's own directory so modules/, iecpoc/, pyproject.toml +# and .venv resolve the same way whether invoked as ./launch.sh or by full path. +HERE="$(cd "$(dirname "$0")" && pwd)" +cd "$HERE" + +# ---- pretty output (color only on a tty) -------------------------------- +if [ -t 1 ]; then + C_OK=$'\033[32m'; C_ERR=$'\033[31m'; C_INFO=$'\033[36m'; C_RST=$'\033[0m' +else + C_OK=""; C_ERR=""; C_INFO=""; C_RST="" +fi +ok() { printf '%s OK %s %s\n' "$C_OK" "$C_RST" "$*"; } +info() { printf '%s ==>%s %s\n' "$C_INFO" "$C_RST" "$*"; } +die() { printf '%sFAIL%s %s\n' "$C_ERR" "$C_RST" "$*" >&2; exit 1; } + +# ---- always unload on the way out --------------------------------------- +cleanup() { + if [ "$LOADED" = 1 ] && lsmod | grep -q "^${MODULE}\b"; then + info "rmmod ${MODULE} (releases the bus, removes ${DEVICE})" + rmmod "$MODULE" 2>/dev/null && ok "unloaded - Pi left as before" \ + || info "rmmod failed; unload manually: sudo rmmod ${MODULE}" + fi +} +trap cleanup EXIT + +# ---- args --------------------------------------------------------------- +# Anything unrecognised (and everything after a literal --) is forwarded to +# iecpoc, so e.g. --raw / --logfile FILE just work. +while [ $# -gt 0 ]; do + case "$1" in + -a|--address) ADDRESS="${2:-}"; shift 2 ;; + --address=*) ADDRESS="${1#*=}"; shift ;; + -d|--device) DEVICE="${2:-}"; shift 2 ;; + --device=*) DEVICE="${1#*=}"; shift ;; + --ko) KO="${2:-}"; shift 2 ;; + --ko=*) KO="${1#*=}"; shift ;; + --) shift; PASS+=("$@"); break ;; + -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) PASS+=("$1"); shift ;; + esac +done + +# ---- preconditions ------------------------------------------------------ +[ "$(id -u)" -eq 0 ] || die "must run as root (use: sudo $0 ...)" +case "$ADDRESS" in ''|*[!0-9]*) die "address must be 0-30 (got '$ADDRESS')" ;; esac +{ [ "$ADDRESS" -ge 0 ] && [ "$ADDRESS" -le 30 ]; } || die "address out of range 0-30: $ADDRESS" +command -v modinfo >/dev/null || die "modinfo not found (install kmod)" +command -v python3 >/dev/null || die "python3 not found (sudo apt install python3)" + +# ---- 1. python frontend: venv + install (once) -------------------------- +# iecpoc declares no third-party runtime deps, so this is offline-safe except +# for the build backend (setuptools), which pip fetches the first time. The +# install is skipped on later runs once the entry point exists. +[ -f "$HERE/pyproject.toml" ] || die "pyproject.toml not found next to $0; package incomplete?" +if [ ! -x "$VENV/bin/iecpoc" ]; then + if [ ! -x "$VENV/bin/python" ]; then + info "creating virtualenv: $VENV" + python3 -m venv "$VENV" || die "could not create venv (sudo apt install python3-venv)" + fi + info "installing iecpoc frontend into $VENV" + "$VENV/bin/pip" install --quiet --disable-pip-version-check . \ + || die "pip install failed (needs network for the build backend, or: sudo apt install python3-pip)" + ok "frontend installed" +fi + +# ---- 2. pick the module matching the running kernel ---------------------- +# Same vermagic-based selection as selftest.sh: match by what the .ko was built +# against (uname -r), not by filename. See selftest.sh for the full rationale. +RUNNING="$(uname -r)" +if [ -z "$KO" ]; then + info "running kernel: $RUNNING" + for dir in "$HERE/modules" "$HERE" "$HERE/kernel"; do + [ -d "$dir" ] || continue + for cand in "$dir"/${MODULE}*.ko; do + [ -f "$cand" ] || continue + vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" && [ -n "$vm" ] || continue + if [ "${vm%% *}" = "$RUNNING" ]; then KO="$cand"; break 2; fi + done + done +fi +[ -n "$KO" ] && [ -f "$KO" ] || die \ +"no module matching the running kernel ($RUNNING) found. + Pass one with --ko PATH, or run ./selftest.sh to list what's available." + +VERMAGIC="$(modinfo -F vermagic "$KO" 2>/dev/null)" || die "cannot read vermagic from $KO" +[ "${VERMAGIC%% *}" = "$RUNNING" ] || die \ +"kernel mismatch: module built for '${VERMAGIC%% *}' but running '$RUNNING'." +info "module: $KO" + +# ---- 3. load (reload if a stale copy is already in) --------------------- +if lsmod | grep -q "^${MODULE}\b"; then + info "a copy is already loaded; removing it first" + rmmod "$MODULE" || die "rmmod failed (is ${DEVICE} in use?)" +fi +info "insmod ${MODULE} address=${ADDRESS}" +insmod "$KO" address="$ADDRESS" || die "insmod failed; check: dmesg | tail" +LOADED=1 +[ -c "$DEVICE" ] || die "${DEVICE} was not created" +ok "loaded; ${DEVICE} present" + +# ---- 4. run the frontend (foreground; rmmod runs in the EXIT trap) ------- +info "running iecpoc (Ctrl-C to stop) - address ${ADDRESS}, device ${DEVICE}" +echo +set +e +"$VENV/bin/iecpoc" --address "$ADDRESS" --device "$DEVICE" "${PASS[@]}" +ST=$? +set -e + +echo +info "frontend exited (status $ST); unloading module" +exit "$ST" From a5406f503c60f06b3f3054af15ad9396d96f6117 Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 05:36:06 +0200 Subject: [PATCH 09/10] feat(kernel): Add PR packaging and artifact upload for testing Enable packaging and artifact upload for pull requests to facilitate testing. Package job generates a zip archive identical to release artifacts, allowing smoke tests before final releases. Adjusted naming for zip files to include PR reference or SHA for traceability. --- .gitea/workflows/build-kernel.yml | 53 ++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/.gitea/workflows/build-kernel.yml b/.gitea/workflows/build-kernel.yml index 809a2f6..83224ac 100644 --- a/.gitea/workflows/build-kernel.yml +++ b/.gitea/workflows/build-kernel.yml @@ -99,14 +99,14 @@ jobs: path: kernel/out if-no-files-found: error - # Release flow only: merges all built modules into one modules/ folder, wraps it - # in a single repo-named top folder alongside the helper scripts (selftest.sh, - # launch.sh) and the iecpoc Python frontend (so launch.sh can pip-install it on - # the Pi), zips it and attaches it to the Gitea release for the pushed tag. - # Skipped entirely on pull requests. - release: + # Merge all built modules into one modules/ folder, wrap it in a single + # repo-named top folder alongside the helper scripts (selftest.sh, launch.sh) + # and the iecpoc Python frontend (so launch.sh can pip-install it on the Pi), + # and zip it. Runs on BOTH triggers and uploads the zip as an artifact: on a PR + # this lets you download and smoke-test the exact archive a release would ship; + # on a tag the release job below republishes this same artifact unchanged. + package: needs: build - if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - name: Checkout @@ -116,10 +116,20 @@ jobs: uses: https://github.com/christopherHX/gitea-download-artifact@v4 with: path: dist - # Merge every artifact back into the shared modules/ folder. + # Merge every per-kernel artifact back into the shared modules/ folder. merge-multiple: true - - name: Assemble release package + - name: Determine version label + id: ver + run: | + # Tags ship under the tag name; PR builds get a traceable pr-. + case "${{ github.ref }}" in + refs/tags/*) v="${{ github.ref_name }}" ;; + *) v="pr${{ github.event.pull_request.number }}-$(echo "${{ github.sha }}" | cut -c1-7)" ;; + esac + echo "ver=$v" >> "$GITHUB_OUTPUT" + + - name: Assemble package zip run: | repo="${{ github.event.repository.name }}" root="package/$repo" @@ -133,11 +143,32 @@ jobs: cp pyproject.toml README.md "$root"/ cp -r iecpoc "$root"/ find "$root/iecpoc" -name __pycache__ -type d -prune -exec rm -rf {} + - (cd package && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo") + (cd package && zip -r "../${repo}-${{ steps.ver.outputs.ver }}.zip" "$repo") + + - name: Upload package zip + uses: https://github.com/christopherHX/gitea-upload-artifact@v4 + with: + name: ${{ github.event.repository.name }}-${{ steps.ver.outputs.ver }} + path: ${{ github.event.repository.name }}-*.zip + if-no-files-found: error + + # Release flow only: takes the zip the package job already built and attaches it + # to the Gitea release for the pushed tag. No re-assembly here. Skipped on PRs. + release: + needs: package + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Download package zip + uses: https://github.com/christopherHX/gitea-download-artifact@v4 + with: + # The single artifact the package job produced for this tag. + name: ${{ github.event.repository.name }}-${{ github.ref_name }} + path: dist - name: Publish to Gitea release uses: https://gitea.com/actions/gitea-release-action@v1.3.6 with: api_key: ${{ secrets.GITEA_TOKEN }} files: |- - *.zip + dist/*.zip From 27ff299f43e24fde293af5dd65f8ae8ec3ad9e32 Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Sat, 20 Jun 2026 05:42:55 +0200 Subject: [PATCH 10/10] feat(kernel): Upload package tree instead of pre-zipped artifact Switch to uploading the assembled package directory as the artifact, allowing the platform to encapsulate it in a single-layer zip. This avoids nested zips and ensures the release process reuses the same archive structure for consistency. --- .gitea/workflows/build-kernel.yml | 39 ++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/build-kernel.yml b/.gitea/workflows/build-kernel.yml index 83224ac..de478e9 100644 --- a/.gitea/workflows/build-kernel.yml +++ b/.gitea/workflows/build-kernel.yml @@ -101,10 +101,15 @@ jobs: # Merge all built modules into one modules/ folder, wrap it in a single # repo-named top folder alongside the helper scripts (selftest.sh, launch.sh) - # and the iecpoc Python frontend (so launch.sh can pip-install it on the Pi), - # and zip it. Runs on BOTH triggers and uploads the zip as an artifact: on a PR - # this lets you download and smoke-test the exact archive a release would ship; - # on a tag the release job below republishes this same artifact unchanged. + # and the iecpoc Python frontend (so launch.sh can pip-install it on the Pi). + # Runs on BOTH triggers and uploads the assembled FOLDER as an artifact: on a PR + # this lets you download and smoke-test the exact tree a release would ship; on a + # tag the release job below zips this same tree and attaches it. + # + # We intentionally upload the folder, not a pre-made .zip: artifacts are always + # transported as a zip by the platform, so uploading a .zip would nest one zip + # inside another. Uploading the tree makes the platform's wrapper zip *be* the + # package archive (a single, clean layer). package: needs: build runs-on: ubuntu-latest @@ -129,7 +134,7 @@ jobs: esac echo "ver=$v" >> "$GITHUB_OUTPUT" - - name: Assemble package zip + - name: Assemble package tree run: | repo="${{ github.event.repository.name }}" root="package/$repo" @@ -143,32 +148,40 @@ jobs: cp pyproject.toml README.md "$root"/ cp -r iecpoc "$root"/ find "$root/iecpoc" -name __pycache__ -type d -prune -exec rm -rf {} + - (cd package && zip -r "../${repo}-${{ steps.ver.outputs.ver }}.zip" "$repo") - - name: Upload package zip + - name: Upload package uses: https://github.com/christopherHX/gitea-upload-artifact@v4 with: name: ${{ github.event.repository.name }}-${{ steps.ver.outputs.ver }} - path: ${{ github.event.repository.name }}-*.zip + # Upload the contents of package/, so the artifact zip contains the + # single top folder package// -> /... + path: package if-no-files-found: error - # Release flow only: takes the zip the package job already built and attaches it - # to the Gitea release for the pushed tag. No re-assembly here. Skipped on PRs. + # Release flow only: download the assembled tree the package job uploaded, zip it + # into the named release asset and attach it to the Gitea release for the pushed + # tag. No re-assembly here (the tree is taken as-is). Skipped on PRs. release: needs: package if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - name: Download package zip + - name: Download package uses: https://github.com/christopherHX/gitea-download-artifact@v4 with: - # The single artifact the package job produced for this tag. + # The single artifact the package job produced for this tag; extracts to + # dist//... name: ${{ github.event.repository.name }}-${{ github.ref_name }} path: dist + - name: Zip release asset + run: | + repo="${{ github.event.repository.name }}" + (cd dist && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo") + - name: Publish to Gitea release uses: https://gitea.com/actions/gitea-release-action@v1.3.6 with: api_key: ${{ secrets.GITEA_TOKEN }} files: |- - dist/*.zip + ${{ github.event.repository.name }}-${{ github.ref_name }}.zip