From 765c98c71b2610ea974a85f7530fd682d6e971ab Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Thu, 23 Apr 2026 15:31:40 -0500 Subject: [PATCH 01/69] Add baseline docs and rework workflow. Establish `rework` as the active development branch and add `CHANGES.md`/`TODO.md` to keep changes and work items tracked alongside the test harness. Made-with: Cursor --- CHANGES.md | 4 ++++ README.md | 31 +++++++++++++++++++++++++++++-- TODO.md | 6 ++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 CHANGES.md create mode 100644 TODO.md diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..a12cd6a --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,4 @@ +## Unreleased + +- Start `rework` branch workflow and add baseline project docs (`README.md`, `CHANGES.md`, `TODO.md`). + diff --git a/README.md b/README.md index 1b7e986..20ffc8d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# test -test code for CI and unit testing +# v9fs test harness + +This repository contains **test code and scripts** for exercising the Linux **9p (v9fs)** filesystem. + +The kernel source under test lives in the upstream repository: + +- `https://github.com/v9fs/linux` + +## What lives here + +- **CI workflows**: automation to build/run tests against a chosen kernel revision +- **Test code**: focused repros and regression tests for v9fs behavior +- **Scripts**: helpers to run locally and/or in CI + +## How we work in this repo + +- **Development branch**: all active work happens on `rework` +- **Change log**: keep `CHANGES.md` updated for every change set +- **Work tracking**: keep `TODO.md` updated as items are added/removed + +## Quick start + +This repo intentionally does *not* vendor the kernel source. Most workflows/scripts will: + +1. Fetch `github.com/v9fs/linux` (or a fork/branch you specify) +2. Build the kernel (or use a provided artifact) +3. Run the tests in this repository against that kernel + +If you tell me how you typically run tests here (CI only vs local/QEMU/VM), I can document the exact command sequence in this section. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..2286387 --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ +## TODO + +- Document the canonical way to run the test suite locally (QEMU/VM vs host). +- Document how to pin the kernel-under-test (fork/branch/commit) for CI and local runs. +- Add a minimal “smoke” target that validates mount + basic IO quickly. + From 9e3d69f3ebbdf6df41926b649ebbcceae0c5419a Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Thu, 23 Apr 2026 15:43:29 -0500 Subject: [PATCH 02/69] Add Docker+QEMU test environment and align CI. Introduce a repo-local Docker image that builds the kernel and runs the existing v9fs test harness under QEMU, and update GitHub Actions workflows to run using the same containerized environment used for local macOS development. Made-with: Cursor --- .github/workflows/demand.yml | 122 ++++++++++++---------------------- .github/workflows/nightly.yml | 115 ++++++++++++-------------------- CHANGES.md | 1 + Dockerfile | 61 +++++++++++++++++ README.md | 29 +++++++- TODO.md | 4 +- scripts/cpu | 60 +++++++++++++++++ scripts/v9fs-build-initrd | 92 +++++++++++++++++++++++++ scripts/v9fs-build-kernel | 17 +++++ scripts/v9fs-entrypoint | 17 +++++ scripts/v9fs-run-tests | 12 ++++ test.bash | 2 +- 12 files changed, 374 insertions(+), 158 deletions(-) create mode 100644 Dockerfile create mode 100644 scripts/cpu create mode 100644 scripts/v9fs-build-initrd create mode 100644 scripts/v9fs-build-kernel create mode 100644 scripts/v9fs-entrypoint create mode 100644 scripts/v9fs-run-tests diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 4bcd02b..53cc092 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -19,85 +19,47 @@ on: type: string workflow_call: jobs: - mainline: - runs-on: self-hosted - container: - image: ghcr.io/v9fs/docker:latest - options: -u root + run: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - repository: ${{ inputs.repo }} - ref: ${{ inputs.ref }} - # TBD: - build: - runs-on: self-hosted - container: - image: ghcr.io/v9fs/docker:latest - options: -u root - steps: - - uses: actions/checkout@v4 - with: - repository: ${{ inputs.repo }} - ref: ${{ inputs.ref }} - - name: configure - run: mkdir -p .build && make O=.build defconfig - - name: make - run: make -j16 O=.build - - uses: actions/upload-artifact@v4 - with: - name: kernel-image - path: .build/arch/*/boot/*Image - retention-days: 1 - test: - needs: latency - runs-on: self-hosted - container: - image: ghcr.io/v9fs/docker:latest - options: -u root - steps: - - uses: actions/download-artifact@v4 - with: - name: kernel-image - path: .build/arch/*/boot/*Image - - name: link workspaces to scratch directory - run: mkdir -p /workspaces && ln -s `pwd` /workspaces/linux - - name: Test sweep - run: | - cd /home/v9fs-test/test - git config --global --add safe.directory /home/v9fs-test/test - git pull - ./test.bash short ci - - name: Store Logs - uses: actions/upload-artifact@v4 - with: - name: test-results - path: /home/v9fs-test/test/logs - retention-days: 7 - latency: - needs: build - runs-on: self-hosted - container: - image: ghcr.io/v9fs/docker:latest - options: -u root - steps: - - uses: actions/download-artifact@v4 - with: - name: kernel-image - path: .build/arch/*/boot/*Image - - name: link workspaces to scratch directory - run: mkdir -p /workspaces && ln -s `pwd` /workspaces/linux - - name: Test sweep - run: | - cd /home/v9fs-test/test - git config --global --add safe.directory /home/v9fs-test/test - git pull - ./test.bash short latency - echo ${{ inputs.resultstag }} ${{ inputs.repo }} ${{ inputs.ref }} > /home/v9fs-test/test/logs/tag.txt - - name: Store Logs - uses: actions/upload-artifact@v4 - with: - name: test-results - path: /home/v9fs-test/test/logs - retention-days: 7 + - name: Checkout test harness + uses: actions/checkout@v4 + + - name: Checkout kernel under test + uses: actions/checkout@v4 + with: + repository: ${{ inputs.repo }} + ref: ${{ inputs.ref }} + path: linux + + - name: Build Docker environment + run: docker build -t v9fs-test-env:local . + + - name: Build kernel + run CI tests (QEMU) + run: | + mkdir -p "$GITHUB_WORKSPACE/tmp" + docker run --rm --privileged \ + -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ + -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-build-kernel && v9fs-run-tests short ci" + + - name: Build kernel + run latency tests (QEMU) + run: | + docker run --rm --privileged \ + -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ + -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-build-kernel && v9fs-run-tests short latency && echo ${{ inputs.resultstag }} ${{ inputs.repo }} ${{ inputs.ref }} > /home/v9fs-test/test/logs/tag.txt" + + - name: Store Logs + uses: actions/upload-artifact@v4 + with: + name: test-results + path: logs + retention-days: 7 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7c633dd..6a77759 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -5,78 +5,47 @@ on: - cron: '0 0 * * *' # Run every day at midnight workflow_dispatch: jobs: - build: - runs-on: self-hosted - container: - image: ghcr.io/v9fs/docker:latest - options: -u root + nightly: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - repository: 'torvalds/linux' - ref: 'master' - - name: configure - run: mkdir -p .build && make O=.build defconfig - - name: make - run: make -j16 O=.build - - uses: actions/upload-artifact@v4 - with: - name: kernel-image - path: .build/arch/*/boot/*Image - retention-days: 1 - test: - needs: latency - runs-on: self-hosted - container: - image: ghcr.io/v9fs/docker:latest - options: -u root - volumes: - - test:/workspaces/tmp - steps: - - uses: actions/download-artifact@v4 - with: - name: kernel-image - path: .build/arch/*/boot/*Image - - name: link workspaces to scratch directory - run: mkdir -p /workspaces && ln -s `pwd` /workspaces/linux - - name: Test sweep - run: | - cd /home/v9fs-test/test - git config --global --add safe.directory /home/v9fs-test/test - git pull - ./test.bash regress ci - - name: Store Logs - uses: actions/upload-artifact@v4 - with: - name: test-results - path: /home/v9fs-test/test/logs - retention-days: 7 - latency: - needs: build - runs-on: self-hosted - container: - image: ghcr.io/v9fs/docker:latest - options: -u root - volumes: - - test:/workspaces/tmp - steps: - - uses: actions/download-artifact@v4 - with: - name: kernel-image - path: .build/arch/*/boot/*Image - - name: link workspaces to scratch directory - run: mkdir -p /workspaces && ln -s `pwd` /workspaces/linux - - name: Test sweep - run: | - cd /home/v9fs-test/test - git config --global --add safe.directory /home/v9fs-test/test - git pull - ./test.bash short latency - echo Nightly torvalds/linux master > /home/v9fs-test/test/logs/tag.txt - - name: Store Logs - uses: actions/upload-artifact@v4 - with: - name: test-results - path: /home/v9fs-test/test/logs - retention-days: 7 + - name: Checkout test harness + uses: actions/checkout@v4 + + - name: Checkout kernel under test + uses: actions/checkout@v4 + with: + repository: 'v9fs/linux' + ref: 'master' + path: linux + + - name: Build Docker environment + run: docker build -t v9fs-test-env:local . + + - name: Build kernel + run regression tests (QEMU) + run: | + mkdir -p "$GITHUB_WORKSPACE/tmp" + docker run --rm --privileged \ + -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ + -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-build-kernel && v9fs-run-tests regress ci" + + - name: Build kernel + run latency tests (QEMU) + run: | + docker run --rm --privileged \ + -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ + -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-build-kernel && v9fs-run-tests short latency && echo Nightly v9fs/linux master > /home/v9fs-test/test/logs/tag.txt" + + - name: Store Logs + uses: actions/upload-artifact@v4 + with: + name: test-results + path: logs + retention-days: 7 diff --git a/CHANGES.md b/CHANGES.md index a12cd6a..975271c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,5 @@ ## Unreleased - Start `rework` branch workflow and add baseline project docs (`README.md`, `CHANGES.md`, `TODO.md`). +- Add Docker + QEMU environment for local macOS runs and align GitHub Actions to use it. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..047a91f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + bc \ + bison \ + build-essential \ + ca-certificates \ + cpio \ + curl \ + file \ + flex \ + git \ + iproute2 \ + kmod \ + libelf-dev \ + libssl-dev \ + make \ + xfsprogs \ + openssh-client \ + python3 \ + qemu-system-x86 \ + qemu-utils \ + rsync \ + time \ + util-linux \ + busybox-static \ + dropbear-bin \ + dbench \ + postmark \ + dwarves \ + && rm -rf /var/lib/apt/lists/* + +# Build standalone `fsx` from FreeBSD source (works across Ubuntu arches). +RUN curl -fsSL https://raw.githubusercontent.com/freebsd/freebsd-src/main/tools/regression/fsx/fsx.c -o /tmp/fsx.c \ + && gcc -O2 -D_GNU_SOURCE -D_POSIX_C_SOURCE=200809L -include stdint.h -include time.h -Wall -Wextra /tmp/fsx.c -o /usr/local/bin/fsx \ + && rm -f /tmp/fsx.c + +# Create a stable user/home matching existing scripts +RUN useradd -m -s /bin/bash v9fs-test \ + && mkdir -p /workspaces /workspaces/tmp \ + && mkdir -p /home/v9fs-test/.ssh \ + && chown -R v9fs-test:v9fs-test /home/v9fs-test /workspaces + +# Provide a shared testbin layout compatible with existing scripts. +# These paths are reachable from the guest via 9p (mounted at /mnt/9). +RUN mkdir -p /home/v9fs-test/testbin/dbench /home/v9fs-test/testbin/postmark /home/v9fs-test/testbin/fsx \ + && ln -sf /usr/bin/dbench /home/v9fs-test/testbin/dbench/dbench \ + && (test -f /usr/share/dbench/client.txt && ln -sf /usr/share/dbench/client.txt /home/v9fs-test/testbin/dbench/client.txt || true) \ + && ln -sf /usr/bin/postmark /home/v9fs-test/testbin/postmark/postmark \ + && ln -sf /usr/local/bin/fsx /home/v9fs-test/testbin/fsx/fsx \ + && chown -R v9fs-test:v9fs-test /home/v9fs-test/testbin + +COPY scripts/ /usr/local/bin/ +RUN chmod +x /usr/local/bin/* + +WORKDIR /home/v9fs-test/test + +ENTRYPOINT ["/usr/local/bin/v9fs-entrypoint"] diff --git a/README.md b/README.md index 20ffc8d..1ce2198 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,31 @@ This repo intentionally does *not* vendor the kernel source. Most workflows/scri 2. Build the kernel (or use a provided artifact) 3. Run the tests in this repository against that kernel -If you tell me how you typically run tests here (CI only vs local/QEMU/VM), I can document the exact command sequence in this section. +### Local (macOS) via Docker + QEMU + +Clone the kernel repo beside this repo: + +```bash +git clone https://github.com/v9fs/linux ../linux +``` + +Build the test environment image: + +```bash +docker build -t v9fs-test-env:local . +``` + +Build the kernel and run tests under QEMU: + +```bash +mkdir -p ./tmp +docker run --rm --privileged \ + -v "$PWD:/home/v9fs-test/test" \ + -v "$PWD/../linux:/workspaces/linux" \ + -v "$PWD/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-build-kernel && v9fs-run-tests short ci" +``` + +Logs are written under `logs/`. \ No newline at end of file diff --git a/TODO.md b/TODO.md index 2286387..9f7bc40 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,4 @@ ## TODO -- Document the canonical way to run the test suite locally (QEMU/VM vs host). -- Document how to pin the kernel-under-test (fork/branch/commit) for CI and local runs. - Add a minimal “smoke” target that validates mount + basic IO quickly. - +- Decide whether to keep `ubuntu-latest` runners or switch back to self-hosted for KVM acceleration. \ No newline at end of file diff --git a/scripts/cpu b/scripts/cpu new file mode 100644 index 0000000..df6330c --- /dev/null +++ b/scripts/cpu @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +key="" +fstab="" + +usage() { + echo "usage: cpu --key [--fstab ] " >&2 + exit 2 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --key) key="$2"; shift 2 ;; + --fstab) fstab="$2"; shift 2 ;; + --help|-h) usage ;; + --*) echo "unknown option: $1" >&2; usage ;; + *) break ;; + esac +done + +[[ $# -ge 2 ]] || usage +[[ -n "$key" ]] || usage + +host="$1"; shift +cmd="$1"; shift || true + +ssh_opts=( + -i "$key" + -p 17010 + -o StrictHostKeyChecking=no + -o UserKnownHostsFile=/dev/null + -o LogLevel=ERROR +) + +scp_opts=("${ssh_opts[@]}") + +if [[ -n "$fstab" ]]; then + scp "${scp_opts[@]}" "$fstab" "root@${host}:/etc/fstab" >/dev/null + ssh "${ssh_opts[@]}" "root@${host}" "mkdir -p /mnt/9 /mnt/root && mount -a || true" >/dev/null +fi + +# Map host-side LOG (relative to repo) into the guest via 9p. +repo_on_guest="/mnt/9/home/v9fs-test/test" +log_host="${LOG:-logs/cpu}" +log_guest="${repo_on_guest}/${log_host}" + +testbin_host="${TESTBIN:-/home/v9fs-test/testbin}" +testbin_guest="/mnt/9${testbin_host}" + +if [[ -f "$cmd" ]]; then + tmp="/tmp/cpu-script.bash" + scp "${scp_opts[@]}" "$cmd" "root@${host}:${tmp}" >/dev/null + ssh "${ssh_opts[@]}" "root@${host}" \ + "cd '${repo_on_guest}' && PATH_MNTDIR='${PATH_MNTDIR:-/mnt/9/workspaces/tmp}' TESTBIN='${testbin_guest}' LOG='${log_guest}' bash '${tmp}'" +else + ssh "${ssh_opts[@]}" "root@${host}" \ + "cd '${repo_on_guest}' && PATH_MNTDIR='${PATH_MNTDIR:-/mnt/9/workspaces/tmp}' TESTBIN='${testbin_guest}' LOG='${log_guest}' $cmd $*" +fi + diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd new file mode 100644 index 0000000..df180ab --- /dev/null +++ b/scripts/v9fs-build-initrd @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -euo pipefail + +out="${1:-/home/v9fs-test/initrd.cpio}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +mkdir -p \ + "$work"/{bin,sbin,etc,proc,sys,dev,run,tmp,mnt/9,mnt/root,root,home/v9fs-test/.ssh} + +cat >"$work/init" <<'EOF' +#!/bin/sh +set -eu + +mount -t proc proc /proc +mount -t sysfs sysfs /sys +mount -t devtmpfs devtmpfs /dev || true + +mkdir -p /run /tmp + +ip link set lo up || true +ip link set eth0 up || true +if command -v udhcpc >/dev/null 2>&1; then + udhcpc -i eth0 -q -t 5 -n || true +fi + +mkdir -p /mnt/9 /mnt/root /root + +# If cpu/scp uploaded an fstab, mount it. (Errors are ok; tests will surface failures.) +if [ -f /etc/fstab ]; then + mkdir -p /mnt/root /mnt/9 + mount -a || true +fi + +mkdir -p /home/v9fs-test/.ssh +chmod 700 /home/v9fs-test/.ssh + +if [ -f /home/v9fs-test/.ssh/authorized_keys ]; then + chmod 600 /home/v9fs-test/.ssh/authorized_keys +fi + +# Start ssh server for the host-side `cpu` helper. +if command -v dropbear >/dev/null 2>&1; then + mkdir -p /etc/dropbear + if [ ! -f /etc/dropbear/dropbear_ed25519_host_key ] && command -v dropbearkey >/dev/null 2>&1; then + dropbearkey -t ed25519 -f /etc/dropbear/dropbear_ed25519_host_key >/dev/null 2>&1 || true + fi + dropbear -R -E -p 17010 +fi + +echo "initrd up; ssh on :17010" +exec sh +EOF +chmod +x "$work/init" + +# Minimal /etc files +cat >"$work/etc/passwd" <<'EOF' +root:x:0:0:root:/root:/bin/sh +EOF +cat >"$work/etc/group" <<'EOF' +root:x:0: +EOF + +# Authorized keys for root login (dropbear checks /root/.ssh and /home/ too; we use /home/v9fs-test). +pubkey="$(cat /home/v9fs-test/.ssh/identity.pub)" +mkdir -p "$work/home/v9fs-test/.ssh" +printf '%s\n' "$pubkey" >"$work/home/v9fs-test/.ssh/authorized_keys" + +# Busybox static provides core utils (sh, mount, udhcpc, etc.) +cp /bin/busybox "$work/bin/busybox" +for a in sh mount mkdir ln ls cat echo sleep uname ps kill ip udhcpc; do + ln -s /bin/busybox "$work/bin/$a" 2>/dev/null || true +done + +# Dropbear + keygen (dynamic; copy required libs) +cp /usr/sbin/dropbear "$work/sbin/dropbear" +cp /usr/bin/dropbearkey "$work/bin/dropbearkey" + +copy_libs() { + local bin="$1" + ldd "$bin" | awk '/=> \//{print $3} /^\//{print $1}' | while read -r lib; do + mkdir -p "$work/$(dirname "$lib")" + cp -L "$lib" "$work/$lib" + done +} +copy_libs /usr/sbin/dropbear +copy_libs /usr/bin/dropbearkey + +(cd "$work" && find . -print0 | cpio --null -ov --format=newc) >"$out" + +echo "Wrote initrd to $out" + diff --git a/scripts/v9fs-build-kernel b/scripts/v9fs-build-kernel new file mode 100644 index 0000000..819cb4a --- /dev/null +++ b/scripts/v9fs-build-kernel @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +src="${1:-/workspaces/linux}" +out="${2:-${src}/.build}" +jobs="${JOBS:-$(nproc)}" + +mkdir -p "$out" + +if [[ ! -f "${out}/.config" ]]; then + make -C "$src" O="$out" defconfig +fi + +make -C "$src" O="$out" -j"$jobs" + +echo "Kernel build complete: ${out}" + diff --git a/scripts/v9fs-entrypoint b/scripts/v9fs-entrypoint new file mode 100644 index 0000000..1c67d99 --- /dev/null +++ b/scripts/v9fs-entrypoint @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +mkdir -p /home/v9fs-test/.ssh + +# Generate an SSH keypair used by `cpu` to reach the guest. +if [[ ! -f /home/v9fs-test/.ssh/identity ]]; then + ssh-keygen -t ed25519 -N '' -f /home/v9fs-test/.ssh/identity >/dev/null +fi + +# Build initrd if missing (contains dropbear + boot init). +if [[ ! -f /home/v9fs-test/initrd.cpio ]]; then + v9fs-build-initrd +fi + +exec "$@" + diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests new file mode 100644 index 0000000..7d584b2 --- /dev/null +++ b/scripts/v9fs-run-tests @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +tests="${1:-short}" +type="${2:-ci}" +check="${3:-1}" + +# Default to the container-provided tools exposed to the guest via 9p. +export TESTBIN="${TESTBIN:-/home/v9fs-test/testbin}" + +exec /home/v9fs-test/test/test.bash "$tests" "$type" "$check" + diff --git a/test.bash b/test.bash index ac3808c..3f7f66d 100755 --- a/test.bash +++ b/test.bash @@ -1,7 +1,7 @@ #!/bin/bash export TESTS=${1:-smoke} export PATH_MNTDIR=/mnt/9/workspaces/tmp -export TESTBIN=/home/v9fs-test/diod/tests/kern +export TESTBIN=${TESTBIN:-/home/v9fs-test/diod/tests/kern} export TIMESTAMP=`date +%s` mkdir -p logs/${TIMESTAMP} export QEMULOG=logs/${TIMESTAMP}/qemu.log From b543053901ee88a54ec234a21f6358f44cf345b4 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Thu, 23 Apr 2026 15:47:06 -0500 Subject: [PATCH 03/69] Split kernel build from QEMU test runs. Build the kernel once, export it as a reusable artifact, and run QEMU test suites against that artifact without rebuilding. Publish the kernel image as a GitHub Actions artifact for reuse by other workflows. Made-with: Cursor --- .github/workflows/demand.yml | 75 +++++++++++++++++++++++++++++++---- .github/workflows/nightly.yml | 75 +++++++++++++++++++++++++++++++---- README.md | 22 ++++++++-- scripts/v9fs-export-kernel | 41 +++++++++++++++++++ 4 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 scripts/v9fs-export-kernel diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 53cc092..12c5798 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -19,7 +19,7 @@ on: type: string workflow_call: jobs: - run: + build-kernel: runs-on: ubuntu-latest steps: - name: Checkout test harness @@ -35,26 +35,87 @@ jobs: - name: Build Docker environment run: docker build -t v9fs-test-env:local . - - name: Build kernel + run CI tests (QEMU) + - name: Build kernel (QEMU guest kernel image) run: | - mkdir -p "$GITHUB_WORKSPACE/tmp" + mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" docker run --rm --privileged \ -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-build-kernel && v9fs-run-tests short ci" + bash -lc "v9fs-build-kernel && v9fs-export-kernel /workspaces/linux /workspaces/linux/.build /workspaces/kernel" + + - name: Upload kernel artifact + uses: actions/upload-artifact@v4 + with: + name: kernel-image + path: kernel/.build/arch/*/boot/*Image + retention-days: 7 - - name: Build kernel + run latency tests (QEMU) + test: + needs: build-kernel + runs-on: ubuntu-latest + steps: + - name: Checkout test harness + uses: actions/checkout@v4 + + - name: Download kernel artifact + uses: actions/download-artifact@v4 + with: + name: kernel-image + path: kernel/.build/arch + + - name: Build Docker environment + run: docker build -t v9fs-test-env:local . + + - name: Run CI tests (QEMU) run: | + mkdir -p "$GITHUB_WORKSPACE/tmp" docker run --rm --privileged \ + -e KERNELBUILD=/workspaces/kernel/.build \ -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ - -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ + -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-run-tests short ci" + + - name: Store Logs + uses: actions/upload-artifact@v4 + with: + name: test-results + path: logs + retention-days: 7 + + latency: + needs: build-kernel + runs-on: ubuntu-latest + steps: + - name: Checkout test harness + uses: actions/checkout@v4 + + - name: Download kernel artifact + uses: actions/download-artifact@v4 + with: + name: kernel-image + path: kernel/.build/arch + + - name: Build Docker environment + run: docker build -t v9fs-test-env:local . + + - name: Run latency tests (QEMU) + run: | + mkdir -p "$GITHUB_WORKSPACE/tmp" + docker run --rm --privileged \ + -e KERNELBUILD=/workspaces/kernel/.build \ + -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ + -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-build-kernel && v9fs-run-tests short latency && echo ${{ inputs.resultstag }} ${{ inputs.repo }} ${{ inputs.ref }} > /home/v9fs-test/test/logs/tag.txt" + bash -lc "v9fs-run-tests short latency && echo ${{ inputs.resultstag }} ${{ inputs.repo }} ${{ inputs.ref }} > /home/v9fs-test/test/logs/tag.txt" - name: Store Logs uses: actions/upload-artifact@v4 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6a77759..82b9c61 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -5,7 +5,7 @@ on: - cron: '0 0 * * *' # Run every day at midnight workflow_dispatch: jobs: - nightly: + build-kernel: runs-on: ubuntu-latest steps: - name: Checkout test harness @@ -21,26 +21,87 @@ jobs: - name: Build Docker environment run: docker build -t v9fs-test-env:local . - - name: Build kernel + run regression tests (QEMU) + - name: Build kernel (QEMU guest kernel image) run: | - mkdir -p "$GITHUB_WORKSPACE/tmp" + mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" docker run --rm --privileged \ -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-build-kernel && v9fs-run-tests regress ci" + bash -lc "v9fs-build-kernel && v9fs-export-kernel /workspaces/linux /workspaces/linux/.build /workspaces/kernel" + + - name: Upload kernel artifact + uses: actions/upload-artifact@v4 + with: + name: kernel-image + path: kernel/.build/arch/*/boot/*Image + retention-days: 7 - - name: Build kernel + run latency tests (QEMU) + test: + needs: build-kernel + runs-on: ubuntu-latest + steps: + - name: Checkout test harness + uses: actions/checkout@v4 + + - name: Download kernel artifact + uses: actions/download-artifact@v4 + with: + name: kernel-image + path: kernel/.build/arch + + - name: Build Docker environment + run: docker build -t v9fs-test-env:local . + + - name: Run regression tests (QEMU) run: | + mkdir -p "$GITHUB_WORKSPACE/tmp" docker run --rm --privileged \ + -e KERNELBUILD=/workspaces/kernel/.build \ -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ - -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ + -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-run-tests regress ci" + + - name: Store Logs + uses: actions/upload-artifact@v4 + with: + name: test-results + path: logs + retention-days: 7 + + latency: + needs: build-kernel + runs-on: ubuntu-latest + steps: + - name: Checkout test harness + uses: actions/checkout@v4 + + - name: Download kernel artifact + uses: actions/download-artifact@v4 + with: + name: kernel-image + path: kernel/.build/arch + + - name: Build Docker environment + run: docker build -t v9fs-test-env:local . + + - name: Run latency tests (QEMU) + run: | + mkdir -p "$GITHUB_WORKSPACE/tmp" + docker run --rm --privileged \ + -e KERNELBUILD=/workspaces/kernel/.build \ + -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ + -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-build-kernel && v9fs-run-tests short latency && echo Nightly v9fs/linux master > /home/v9fs-test/test/logs/tag.txt" + bash -lc "v9fs-run-tests short latency && echo Nightly v9fs/linux master > /home/v9fs-test/test/logs/tag.txt" - name: Store Logs uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index 1ce2198..1595bcf 100644 --- a/README.md +++ b/README.md @@ -40,17 +40,31 @@ Build the test environment image: docker build -t v9fs-test-env:local . ``` -Build the kernel and run tests under QEMU: +Build the kernel once (and export it as a reusable artifact): ```bash -mkdir -p ./tmp +mkdir -p ./tmp ./kernel docker run --rm --privileged \ -v "$PWD:/home/v9fs-test/test" \ -v "$PWD/../linux:/workspaces/linux" \ + -v "$PWD/kernel:/workspaces/kernel" \ -v "$PWD/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-build-kernel && v9fs-run-tests short ci" + bash -lc "v9fs-build-kernel && v9fs-export-kernel /workspaces/linux /workspaces/linux/.build /workspaces/kernel" ``` -Logs are written under `logs/`. \ No newline at end of file +Then run tests repeatedly without rebuilding the kernel: + +```bash +docker run --rm --privileged \ + -e KERNELBUILD=/workspaces/kernel/.build \ + -v "$PWD:/home/v9fs-test/test" \ + -v "$PWD/kernel:/workspaces/kernel" \ + -v "$PWD/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-run-tests short ci" +``` + +Logs are written under `logs/`. diff --git a/scripts/v9fs-export-kernel b/scripts/v9fs-export-kernel new file mode 100644 index 0000000..7f884ad --- /dev/null +++ b/scripts/v9fs-export-kernel @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Export the kernel image from a build directory into a stable artifact layout. +# +# Default layout: +# /.build/arch//boot/ + +src="${1:-/workspaces/linux}" +build="${2:-${src}/.build}" +dst="${3:-/workspaces/kernel}" + +arch="${ARCH:-$(uname -m)}" + +mkdir -p "$dst/.build" + +case "$arch" in + aarch64) + img="${build}/arch/arm64/boot/Image" + outdir="$dst/.build/arch/arm64/boot" + ;; + x86_64) + img="${build}/arch/x86_64/boot/bzImage" + outdir="$dst/.build/arch/x86_64/boot" + ;; + *) + echo "Unsupported ARCH=$arch (expected aarch64 or x86_64)" >&2 + exit 2 + ;; +esac + +if [[ ! -f "$img" ]]; then + echo "Kernel image not found at: $img" >&2 + exit 1 +fi + +mkdir -p "$outdir" +cp -f "$img" "$outdir/" + +echo "Exported kernel artifact to: $dst/.build" + From 05111f982f097a3d008000818a007b755800d163 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Thu, 23 Apr 2026 19:20:55 -0500 Subject: [PATCH 04/69] Run v9fs tests in QEMU guest; align CI artifacts - Guest initrd runs v9fs-guest-run from kernel cmdline (Option A)\n- v9fs-run-tests bind-mounts a stable 9p export root\n- Unique GitHub Actions log artifact names; ignore local kernel/tmp outputs\n- Document guest-direct flow in README; update CHANGES/TODO Made-with: Cursor --- .github/workflows/demand.yml | 4 +- .github/workflows/nightly.yml | 4 +- .gitignore | 4 ++ CHANGES.md | 4 ++ Dockerfile | 1 + README.md | 26 ++++++++++++- TODO.md | 4 +- qemu.bash | 10 +++-- scripts/cpu | 14 ++++++- scripts/v9fs-build-initrd | 66 ++++++++++++++++++++++++++------ scripts/v9fs-guest-run | 72 +++++++++++++++++++++++++++++++++++ scripts/v9fs-run-tests | 30 ++++++++++++++- test.bash | 33 ++++++++++++---- 13 files changed, 240 insertions(+), 32 deletions(-) create mode 100755 scripts/v9fs-guest-run diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 12c5798..3099fad 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -85,7 +85,7 @@ jobs: - name: Store Logs uses: actions/upload-artifact@v4 with: - name: test-results + name: test-results-ci path: logs retention-days: 7 @@ -120,7 +120,7 @@ jobs: - name: Store Logs uses: actions/upload-artifact@v4 with: - name: test-results + name: test-results-latency path: logs retention-days: 7 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 82b9c61..b9275d0 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -71,7 +71,7 @@ jobs: - name: Store Logs uses: actions/upload-artifact@v4 with: - name: test-results + name: test-results-regression path: logs retention-days: 7 @@ -106,7 +106,7 @@ jobs: - name: Store Logs uses: actions/upload-artifact@v4 with: - name: test-results + name: test-results-latency path: logs retention-days: 7 diff --git a/.gitignore b/.gitignore index c75dc7c..e2907e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ logs/* +kernel/ +tmp/ +initrd.cpio +qemu.pid diff --git a/CHANGES.md b/CHANGES.md index 975271c..78e6885 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,4 +2,8 @@ - Start `rework` branch workflow and add baseline project docs (`README.md`, `CHANGES.md`, `TODO.md`). - Add Docker + QEMU environment for local macOS runs and align GitHub Actions to use it. +- Run the suite **inside the QEMU guest** via initrd cmdline flags (`v9fs.run=1`, `v9fs.tests`, …) and a small guest runner (`scripts/v9fs-guest-run`), avoiding SSH/host port forwarding. +- Stabilize 9p exports for guest tests by bind-mounting the workspace/kernel/tmp/testbins under `/workspaces/share` (`scripts/v9fs-run-tests`). +- Give nightly/on-demand log artifacts **unique names** so parallel jobs do not collide on `actions/upload-artifact`. +- Ignore common local build outputs (`kernel/`, `tmp/`, generated initrd/pid files) in `.gitignore`. diff --git a/Dockerfile b/Dockerfile index 047a91f..707ad01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ xfsprogs \ openssh-client \ python3 \ + qemu-system-arm \ qemu-system-x86 \ qemu-utils \ rsync \ diff --git a/README.md b/README.md index 1595bcf..bccd901 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ docker run --rm --privileged \ Then run tests repeatedly without rebuilding the kernel: ```bash +mkdir -p ./tmp docker run --rm --privileged \ -e KERNELBUILD=/workspaces/kernel/.build \ -v "$PWD:/home/v9fs-test/test" \ @@ -67,4 +68,27 @@ docker run --rm --privileged \ bash -lc "v9fs-run-tests short ci" ``` -Logs are written under `logs/`. +## How tests run (guest-direct) + +`v9fs-run-tests` boots QEMU with an initrd that mounts the host-exported workspace +over **9p** and then runs the suite **inside the guest** (no SSH/port-forwarding). + +The host-visible output is primarily the QEMU serial log: + +- `logs//qemu.log` + +The guest mounts the repo at `/mnt/9/test` (see `scripts/v9fs-guest-run`). + +## GitHub Actions + +CI uses the same Docker + QEMU flow as local development: + +1. Build the kernel in a container and upload `kernel-image` +2. Download that artifact into `kernel/.build/arch/...` for test jobs +3. Run `v9fs-run-tests ...` with `--privileged` so the harness can bind-mount a + stable 9p export root (`/workspaces/share`) + +Nightly uploads two log bundles with distinct artifact names: + +- `test-results-regression` +- `test-results-latency` \ No newline at end of file diff --git a/TODO.md b/TODO.md index 9f7bc40..e4ad804 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,4 @@ ## TODO -- Add a minimal “smoke” target that validates mount + basic IO quickly. -- Decide whether to keep `ubuntu-latest` runners or switch back to self-hosted for KVM acceleration. \ No newline at end of file +- Decide whether to keep `ubuntu-latest` runners or switch back to self-hosted for KVM acceleration. +- Remove or clearly fence legacy SSH-based helpers (`test.bash`, `scripts/cpu`) if they are no longer part of the supported workflow. \ No newline at end of file diff --git a/qemu.bash b/qemu.bash index 0bfc825..b709bf5 100755 --- a/qemu.bash +++ b/qemu.bash @@ -18,6 +18,7 @@ then export KERNEL="${KERNELBUILD}/arch/arm64/boot/Image" export MACHINE=virt export APPEND="earlycon console=ttyAMA0" + export QEMUCPU=${QEMUCPU:-"cortex-a57"} export EXTRA="" elif [ $ARCH == "x86_64" ] then @@ -25,12 +26,13 @@ then export KERNEL="${KERNELBUILD}/arch/x86_64/boot/bzImage" export MACHINE=q35 export APPEND="console=ttyS0" + export QEMUCPU=${QEMUCPU:-"max"} export EXTRA="-debugcon file:debug.log -global isa-debugcon.iobase=0x402" fi ${QEMU} -kernel \ ${KERNEL} \ - -cpu max \ + -cpu ${QEMUCPU} \ -machine ${MACHINE} \ -s \ -smp 4 \ @@ -39,11 +41,11 @@ ${QEMU} -kernel \ -object rng-random,filename=/dev/urandom,id=rng0 \ -device virtio-rng-pci,rng=rng0 \ -device virtio-net-pci,netdev=n1 \ - -netdev user,id=n1,hostfwd=tcp:127.0.0.1:17010-:17010,net=192.168.1.0/24,host=192.168.1.1 \ + -netdev user,id=n1 \ -serial file:${LOG} \ - -fsdev local,security_model=none,id=fsdev0,path=/ \ + -fsdev local,security_model=none,writeout=immediate,id=fsdev0,path=${FSDEV_PATH:-/} \ -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare \ - -append "${APPEND}" \ + -append "${APPEND} ${EXTRA_APPEND:-}" \ ${EXTRA} \ -daemonize -display none -pidfile ${PIDFILE} diff --git a/scripts/cpu b/scripts/cpu index df6330c..0ffb2d4 100644 --- a/scripts/cpu +++ b/scripts/cpu @@ -28,12 +28,24 @@ cmd="$1"; shift || true ssh_opts=( -i "$key" -p 17010 + -o BatchMode=yes + -o ConnectTimeout=2 + -o ConnectionAttempts=1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ) -scp_opts=("${ssh_opts[@]}") +scp_opts=( + -i "$key" + -P 17010 + -o BatchMode=yes + -o ConnectTimeout=2 + -o ConnectionAttempts=1 + -o StrictHostKeyChecking=no + -o UserKnownHostsFile=/dev/null + -o LogLevel=ERROR +) if [[ -n "$fstab" ]]; then scp "${scp_opts[@]}" "$fstab" "root@${host}:/etc/fstab" >/dev/null diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index df180ab..7c406e9 100644 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -6,15 +6,19 @@ work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT mkdir -p \ - "$work"/{bin,sbin,etc,proc,sys,dev,run,tmp,mnt/9,mnt/root,root,home/v9fs-test/.ssh} + "$work"/{bin,sbin,etc,proc,sys,dev,run,tmp,mnt/9,mnt/root,root,root/.ssh,home/v9fs-test/.ssh} cat >"$work/init" <<'EOF' #!/bin/sh set -eu +export PATH=/bin:/sbin + mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev || true +mkdir -p /dev/pts +mount -t devpts devpts /dev/pts || true mkdir -p /run /tmp @@ -26,6 +30,11 @@ fi mkdir -p /mnt/9 /mnt/root /root +# Mount hostshare so we can access the test repo/scripts. +if ! mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then + echo "initrd: WARNING: failed to mount hostshare on /mnt/9" +fi + # If cpu/scp uploaded an fstab, mount it. (Errors are ok; tests will surface failures.) if [ -f /etc/fstab ]; then mkdir -p /mnt/root /mnt/9 @@ -34,21 +43,42 @@ fi mkdir -p /home/v9fs-test/.ssh chmod 700 /home/v9fs-test/.ssh +mkdir -p /root/.ssh +chmod 700 /root/.ssh if [ -f /home/v9fs-test/.ssh/authorized_keys ]; then chmod 600 /home/v9fs-test/.ssh/authorized_keys fi - -# Start ssh server for the host-side `cpu` helper. -if command -v dropbear >/dev/null 2>&1; then - mkdir -p /etc/dropbear - if [ ! -f /etc/dropbear/dropbear_ed25519_host_key ] && command -v dropbearkey >/dev/null 2>&1; then - dropbearkey -t ed25519 -f /etc/dropbear/dropbear_ed25519_host_key >/dev/null 2>&1 || true - fi - dropbear -R -E -p 17010 +if [ -f /root/.ssh/authorized_keys ]; then + chmod 600 /root/.ssh/authorized_keys fi -echo "initrd up; ssh on :17010" +# Start ssh server for the host-side `cpu` helper. +# Option A: run tests directly in guest (no SSH transport). +cmdline="$(cat /proc/cmdline 2>/dev/null || true)" +case "$cmdline" in + *v9fs.run=1*) + tests="$(echo "$cmdline" | sed -n 's/.*v9fs.tests=\([^ ]*\).*/\1/p')" + type="$(echo "$cmdline" | sed -n 's/.*v9fs.type=\([^ ]*\).*/\1/p')" + check="$(echo "$cmdline" | sed -n 's/.*v9fs.check=\([^ ]*\).*/\1/p')" + : "${tests:=smoke}" + : "${type:=ci}" + : "${check:=1}" + + echo "initrd: running guest tests tests=$tests type=$type check=$check" + if [ -f /mnt/9/test/scripts/v9fs-guest-run ]; then + /bin/sh /mnt/9/test/scripts/v9fs-guest-run "$tests" "$type" "$check" || true + else + echo "initrd: missing /mnt/9/test/scripts/v9fs-guest-run" + ls -la /mnt/9/test/scripts 2>/dev/null || true + ls -la /mnt/9/test 2>/dev/null || true + fi + echo "initrd: powering off" + poweroff -f || halt -f || reboot -f + ;; +esac + +echo "initrd up; interactive shell" exec sh EOF chmod +x "$work/init" @@ -65,10 +95,18 @@ EOF pubkey="$(cat /home/v9fs-test/.ssh/identity.pub)" mkdir -p "$work/home/v9fs-test/.ssh" printf '%s\n' "$pubkey" >"$work/home/v9fs-test/.ssh/authorized_keys" +mkdir -p "$work/root/.ssh" +printf '%s\n' "$pubkey" >"$work/root/.ssh/authorized_keys" + +# Pre-generate a host key for dropbear so boot doesn't block on keygen. +mkdir -p "$work/etc/dropbear" +if command -v dropbearkey >/dev/null 2>&1; then + dropbearkey -t ed25519 -f "$work/etc/dropbear/dropbear_ed25519_host_key" >/dev/null 2>&1 || true +fi # Busybox static provides core utils (sh, mount, udhcpc, etc.) cp /bin/busybox "$work/bin/busybox" -for a in sh mount mkdir ln ls cat echo sleep uname ps kill ip udhcpc; do +for a in sh mount mkdir ln ls cat echo sleep uname ps kill ip udhcpc netstat poweroff halt reboot date awk sed diff cp rm sort; do ln -s /bin/busybox "$work/bin/$a" 2>/dev/null || true done @@ -86,6 +124,12 @@ copy_libs() { copy_libs /usr/sbin/dropbear copy_libs /usr/bin/dropbearkey +# Some environments omit the dynamic loader from `ldd` parsing; ensure it exists. +if [ -f /lib/ld-linux-aarch64.so.1 ]; then + mkdir -p "$work/lib" + cp -L /lib/ld-linux-aarch64.so.1 "$work/lib/ld-linux-aarch64.so.1" +fi + (cd "$work" && find . -print0 | cpio --null -ov --format=newc) >"$out" echo "Wrote initrd to $out" diff --git a/scripts/v9fs-guest-run b/scripts/v9fs-guest-run new file mode 100755 index 0000000..7113407 --- /dev/null +++ b/scripts/v9fs-guest-run @@ -0,0 +1,72 @@ +#!/bin/sh +set -eu + +tests="${1:-smoke}" +type="${2:-ci}" +check="${3:-1}" + +repo="/mnt/9/test" +ts="${TIMESTAMP:-$(date +%s)}" + +PATH_MNTDIR="${PATH_MNTDIR:-/mnt/9/tmp}" +TESTBIN="${TESTBIN:-/mnt/9/testbin}" + +mkdir -p "$repo/logs/$ts" +rm -f "$repo/logs/current" +ln -s "$ts" "$repo/logs/current" + +echo "GUEST: TYPE=$type TESTS=$tests CHECK=$check" +echo "GUEST: repo=$repo" +echo "GUEST: PATH_MNTDIR=$PATH_MNTDIR" +echo "GUEST: TESTBIN=$TESTBIN" + +mkdir -p "$PATH_MNTDIR" +rm -rf "$PATH_MNTDIR"/* + +for f in "$repo"/fstabs-"$tests"/*; do + ft="$(basename "$f")" + mode="$(echo "$ft" | awk -F- '{print $1}')" + cfg="$(echo "$ft" | awk -F- '{print $2}')" + + outdir="$repo/logs/$ts/$mode/$cfg" + mkdir -p "$outdir" + + echo "GUEST: running configuration $mode $cfg" + + # Ensure mountpoints exist + mkdir -p /mnt/9 /mnt/root /dev /sys /proc + + # Apply the provided fstab inside guest + cp "$f" /etc/fstab + umount /mnt/9 2>/dev/null || true + umount /mnt/root 2>/dev/null || true + mount -a || true + + results="/tmp/results.log" + : >"$results" + + for name in dbench fsx fsx-mmap ldconfig postmark; do + t="$repo/tests/$type/$name.bash" + [ -f "$t" ] || continue + echo -n "...running test $name ..." | tee -a "$results" + + LOG="$outdir/$name" + export LOG PATH_MNTDIR TESTBIN + + if /bin/sh "$t" >"$LOG.log" 2>&1; then + echo "SUCCESS" | tee -a "$results" + else + echo "FAIL" | tee -a "$results" + fi + done + + if [ "$check" = "1" ]; then + if ! diff -b "$results" "$repo/good/$cfg/results.log" >/dev/null 2>&1; then + echo "GUEST: Configuration $f FAILED" + cat "$results" + exit 1 + fi + fi +done + +echo "GUEST: tests done" diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 7d584b2..5ea9a3d 100644 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -8,5 +8,33 @@ check="${3:-1}" # Default to the container-provided tools exposed to the guest via 9p. export TESTBIN="${TESTBIN:-/home/v9fs-test/testbin}" -exec /home/v9fs-test/test/test.bash "$tests" "$type" "$check" +export TIMESTAMP="${TIMESTAMP:-$(date +%s)}" +mkdir -p "logs/${TIMESTAMP}" +export QEMULOG="logs/${TIMESTAMP}/qemu.log" +export PIDFILE="/home/v9fs-test/qemu.pid" + +# Build a stable export root so 9p sees our bind mounts. +mkdir -p /workspaces/share +mkdir -p /workspaces/share/test /workspaces/share/tmp /workspaces/share/kernel /workspaces/share/testbin +mountpoint -q /workspaces/share/test || mount --bind /home/v9fs-test/test /workspaces/share/test +mountpoint -q /workspaces/share/tmp || mount --bind /workspaces/tmp /workspaces/share/tmp +mountpoint -q /workspaces/share/kernel || mount --bind /workspaces/kernel /workspaces/share/kernel +mountpoint -q /workspaces/share/testbin || mount --bind /home/v9fs-test/testbin /workspaces/share/testbin + +export FSDEV_PATH="/workspaces/share" + +# Run the suite inside the guest and poweroff when complete. +export EXTRA_APPEND="v9fs.run=1 v9fs.tests=${tests} v9fs.type=${type} v9fs.check=${check}" + +/home/v9fs-test/test/qemu.bash + +# Wait for QEMU to exit (guest powers off). +if [ -f "${PIDFILE}" ]; then + pid="$(cat "${PIDFILE}")" + while kill -0 "${pid}" 2>/dev/null; do + sleep 1 + done +fi + +echo "QEMU run complete; logs at ${QEMULOG}" diff --git a/test.bash b/test.bash index 3f7f66d..e5bd069 100755 --- a/test.bash +++ b/test.bash @@ -10,8 +10,8 @@ export QEMUSTATE=0 export CHECK=${3:-1} export TYPE=${2:-ci} -rm -rf /workspaces/tmp mkdir -p /workspaces/tmp +rm -rf /workspaces/tmp/* if [ "$TYPE" = "ci" ]; then export CHECK=${3:-1} @@ -41,14 +41,29 @@ if test -f ${PIDFILE}; then else echo Starting qemu /home/v9fs-test/test/qemu.bash - sleep 5 + sleep 2 fi -QEMUPID=`cat ${PIDFILE}` -QEMUCMDLINE=`ps -o command -www --noheaders $QEMUPID` -set -- $QEMUCMDLINE -$1 --version -echo $QEMUCMDLINE +if test -f ${PIDFILE}; then + QEMUPID=`cat ${PIDFILE}` + QEMUCMDLINE=`ps -o command -www --noheaders $QEMUPID || true` + set -- $QEMUCMDLINE + if [ -n "${1}" ]; then + $1 --version || true + fi + echo $QEMUCMDLINE +else + echo "WARNING: QEMU pidfile not found at ${PIDFILE}" +fi + +echo Waiting for guest SSH... +for i in $(seq 1 60); do + if cpu --key /home/v9fs-test/.ssh/identity localhost uname -rm >/dev/null 2>&1; then + echo "Guest SSH is up." + break + fi + sleep 1 +done echo Starting tests ${TIMESTAMP} cpu --key /home/v9fs-test/.ssh/identity localhost uname -rm @@ -82,5 +97,7 @@ done cleanup echo tests ${TESTS} ${TYPE} ${TIMESTAMP} done -kill ${QEMUPID} +if [ -n "${QEMUPID:-}" ]; then + kill ${QEMUPID} || true +fi exit 0 From da7b2a37e8f7d3d837b8163a5d559496c026354c Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Thu, 23 Apr 2026 19:44:51 -0500 Subject: [PATCH 05/69] CI: run demand workflow on every push and manual dispatch - Add push trigger (all branches); keep workflow_dispatch + workflow_call\n- Default kernel repo/ref on push to v9fs/linux @ master\n- Latency tag line uses env + commit SHA fallback\n- Rename workflow for clarity; document triggers in README and CHANGES Made-with: Cursor --- .github/workflows/demand.yml | 18 ++++++++++++++---- CHANGES.md | 1 + README.md | 16 ++++++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 3099fad..b604c1d 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -1,6 +1,8 @@ -name: On Demand Regression +name: CI (push and manual) on: + # Run CI on every push (all branches). Narrow with `branches:` if this is too noisy. + push: workflow_dispatch: inputs: repo: @@ -21,6 +23,10 @@ on: jobs: build-kernel: runs-on: ubuntu-latest + env: + # workflow_dispatch / workflow_call populate `inputs`; push does not — use defaults. + KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} + KERNEL_REF: ${{ inputs.ref || 'master' }} steps: - name: Checkout test harness uses: actions/checkout@v4 @@ -28,8 +34,8 @@ jobs: - name: Checkout kernel under test uses: actions/checkout@v4 with: - repository: ${{ inputs.repo }} - ref: ${{ inputs.ref }} + repository: ${{ env.KERNEL_REPO }} + ref: ${{ env.KERNEL_REF }} path: linux - name: Build Docker environment @@ -92,6 +98,10 @@ jobs: latency: needs: build-kernel runs-on: ubuntu-latest + env: + RESULTSTAG: ${{ inputs.resultstag || github.sha }} + KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} + KERNEL_REF: ${{ inputs.ref || 'master' }} steps: - name: Checkout test harness uses: actions/checkout@v4 @@ -115,7 +125,7 @@ jobs: -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-run-tests short latency && echo ${{ inputs.resultstag }} ${{ inputs.repo }} ${{ inputs.ref }} > /home/v9fs-test/test/logs/tag.txt" + bash -lc "v9fs-run-tests short latency && echo \"$RESULTSTAG $KERNEL_REPO $KERNEL_REF\" > /home/v9fs-test/test/logs/tag.txt" - name: Store Logs uses: actions/upload-artifact@v4 diff --git a/CHANGES.md b/CHANGES.md index 78e6885..52f62ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,4 +6,5 @@ - Stabilize 9p exports for guest tests by bind-mounting the workspace/kernel/tmp/testbins under `/workspaces/share` (`scripts/v9fs-run-tests`). - Give nightly/on-demand log artifacts **unique names** so parallel jobs do not collide on `actions/upload-artifact`. - Ignore common local build outputs (`kernel/`, `tmp/`, generated initrd/pid files) in `.gitignore`. +- **CI workflow** (`.github/workflows/demand.yml`): run on **every `push`** as well as **manual** `workflow_dispatch`; default kernel checkout on push is `v9fs/linux` @ `master` (manual runs still use the form defaults, e.g. `ericvh/for-next`). diff --git a/README.md b/README.md index bccd901..98ea995 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,17 @@ CI uses the same Docker + QEMU flow as local development: 1. Build the kernel in a container and upload `kernel-image` 2. Download that artifact into `kernel/.build/arch/...` for test jobs 3. Run `v9fs-run-tests ...` with `--privileged` so the harness can bind-mount a - stable 9p export root (`/workspaces/share`) + stable 9p export root (`/workspaces/share`) -Nightly uploads two log bundles with distinct artifact names: +### Workflows + +| Workflow | File | When it runs | +|----------|------|----------------| +| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Pushes use `v9fs/linux` @ `master` unless you run manually with other inputs. | +| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ `master`; full `regress` + latency jobs. | + +Log artifact names (avoid collisions when jobs run in parallel): + +- CI manual/push: `test-results-ci`, `test-results-latency` +- Nightly: `test-results-regression`, `test-results-latency` -- `test-results-regression` -- `test-results-latency` \ No newline at end of file From eb267c8d63666612bf96931e65e99cbe28dceeff Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Thu, 23 Apr 2026 19:47:37 -0500 Subject: [PATCH 06/69] Fix CI: checkout v9fs/linux default branch ericvh/devel v9fs/linux has no master branch; push CI used ref=master and actions/checkout failed.\nUse ericvh/devel (repo default_branch) for demand push defaults and nightly. Made-with: Cursor --- .github/workflows/demand.yml | 6 ++++-- .github/workflows/nightly.yml | 4 ++-- CHANGES.md | 3 ++- README.md | 10 ++++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index b604c1d..34d6a1e 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -26,7 +26,8 @@ jobs: env: # workflow_dispatch / workflow_call populate `inputs`; push does not — use defaults. KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} - KERNEL_REF: ${{ inputs.ref || 'master' }} + # v9fs/linux default branch is ericvh/devel (there is no master). + KERNEL_REF: ${{ inputs.ref || 'ericvh/devel' }} steps: - name: Checkout test harness uses: actions/checkout@v4 @@ -101,7 +102,8 @@ jobs: env: RESULTSTAG: ${{ inputs.resultstag || github.sha }} KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} - KERNEL_REF: ${{ inputs.ref || 'master' }} + # v9fs/linux default branch is ericvh/devel (there is no master). + KERNEL_REF: ${{ inputs.ref || 'ericvh/devel' }} steps: - name: Checkout test harness uses: actions/checkout@v4 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index b9275d0..3b33bfe 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'v9fs/linux' - ref: 'master' + ref: 'ericvh/devel' path: linux - name: Build Docker environment @@ -101,7 +101,7 @@ jobs: -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-run-tests short latency && echo Nightly v9fs/linux master > /home/v9fs-test/test/logs/tag.txt" + bash -lc "v9fs-run-tests short latency && echo Nightly v9fs/linux ericvh/devel > /home/v9fs-test/test/logs/tag.txt" - name: Store Logs uses: actions/upload-artifact@v4 diff --git a/CHANGES.md b/CHANGES.md index 52f62ff..1325de1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,5 +6,6 @@ - Stabilize 9p exports for guest tests by bind-mounting the workspace/kernel/tmp/testbins under `/workspaces/share` (`scripts/v9fs-run-tests`). - Give nightly/on-demand log artifacts **unique names** so parallel jobs do not collide on `actions/upload-artifact`. - Ignore common local build outputs (`kernel/`, `tmp/`, generated initrd/pid files) in `.gitignore`. -- **CI workflow** (`.github/workflows/demand.yml`): run on **every `push`** as well as **manual** `workflow_dispatch`; default kernel checkout on push is `v9fs/linux` @ `master` (manual runs still use the form defaults, e.g. `ericvh/for-next`). +- **CI workflow** (`.github/workflows/demand.yml`): run on **every `push`** as well as **manual** `workflow_dispatch`; default kernel checkout on push is `v9fs/linux` @ `ericvh/devel` (that repo’s default branch; there is no `master`) (manual runs still use the form defaults, e.g. `ericvh/for-next`). +- **Fix Actions**: align kernel `ref` with `v9fs/linux` (use `ericvh/devel` instead of non-existent `master` in CI and nightly checkouts). diff --git a/README.md b/README.md index 98ea995..2c7ee5a 100644 --- a/README.md +++ b/README.md @@ -90,10 +90,12 @@ CI uses the same Docker + QEMU flow as local development: ### Workflows -| Workflow | File | When it runs | -|----------|------|----------------| -| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Pushes use `v9fs/linux` @ `master` unless you run manually with other inputs. | -| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ `master`; full `regress` + latency jobs. | + +| Workflow | File | When it runs | +| ------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Pushes use `v9fs/linux` @ `ericvh/devel` (repo default) unless you run manually with other inputs. | +| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ `ericvh/devel`; full `regress` + latency jobs. | + Log artifact names (avoid collisions when jobs run in parallel): From f58856ecf4b688333c5ce771c6a067d2bdb06cc8 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Thu, 23 Apr 2026 19:51:25 -0500 Subject: [PATCH 07/69] CI: pin v9fs/linux kernel checkout to main Use main for push defaults, nightly, and workflow_dispatch ref default;\navoid unstable topic-branch dynamics. Made-with: Cursor --- .github/workflows/demand.yml | 10 +++++----- .github/workflows/nightly.yml | 4 ++-- CHANGES.md | 4 ++-- README.md | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 34d6a1e..454e2e7 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -13,7 +13,7 @@ on: ref: description: 'repository reference' required: true - default: 'ericvh/for-next' + default: 'main' type: string resultstag: description: 'how to tag results' @@ -26,8 +26,8 @@ jobs: env: # workflow_dispatch / workflow_call populate `inputs`; push does not — use defaults. KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} - # v9fs/linux default branch is ericvh/devel (there is no master). - KERNEL_REF: ${{ inputs.ref || 'ericvh/devel' }} + # Pin to main for stable CI (avoid volatile topic branches). + KERNEL_REF: ${{ inputs.ref || 'main' }} steps: - name: Checkout test harness uses: actions/checkout@v4 @@ -102,8 +102,8 @@ jobs: env: RESULTSTAG: ${{ inputs.resultstag || github.sha }} KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} - # v9fs/linux default branch is ericvh/devel (there is no master). - KERNEL_REF: ${{ inputs.ref || 'ericvh/devel' }} + # Pin to main for stable CI (avoid volatile topic branches). + KERNEL_REF: ${{ inputs.ref || 'main' }} steps: - name: Checkout test harness uses: actions/checkout@v4 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3b33bfe..15aab39 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'v9fs/linux' - ref: 'ericvh/devel' + ref: 'main' path: linux - name: Build Docker environment @@ -101,7 +101,7 @@ jobs: -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-run-tests short latency && echo Nightly v9fs/linux ericvh/devel > /home/v9fs-test/test/logs/tag.txt" + bash -lc "v9fs-run-tests short latency && echo Nightly v9fs/linux main > /home/v9fs-test/test/logs/tag.txt" - name: Store Logs uses: actions/upload-artifact@v4 diff --git a/CHANGES.md b/CHANGES.md index 1325de1..05f08b4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,6 @@ - Stabilize 9p exports for guest tests by bind-mounting the workspace/kernel/tmp/testbins under `/workspaces/share` (`scripts/v9fs-run-tests`). - Give nightly/on-demand log artifacts **unique names** so parallel jobs do not collide on `actions/upload-artifact`. - Ignore common local build outputs (`kernel/`, `tmp/`, generated initrd/pid files) in `.gitignore`. -- **CI workflow** (`.github/workflows/demand.yml`): run on **every `push`** as well as **manual** `workflow_dispatch`; default kernel checkout on push is `v9fs/linux` @ `ericvh/devel` (that repo’s default branch; there is no `master`) (manual runs still use the form defaults, e.g. `ericvh/for-next`). -- **Fix Actions**: align kernel `ref` with `v9fs/linux` (use `ericvh/devel` instead of non-existent `master` in CI and nightly checkouts). +- **CI workflow** (`.github/workflows/demand.yml`): run on **every `push`** as well as **manual** `workflow_dispatch`; kernel checkout defaults to **`v9fs/linux` @ `main`** (stable; manual dispatch can still override `ref`). +- **Fix Actions**: kernel `ref` pinned to **`main`** where defaults apply (push CI and nightly), avoiding volatile branches on `v9fs/linux`. diff --git a/README.md b/README.md index 2c7ee5a..17f0dd0 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,8 @@ CI uses the same Docker + QEMU flow as local development: | Workflow | File | When it runs | | ------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Pushes use `v9fs/linux` @ `ericvh/devel` (repo default) unless you run manually with other inputs. | -| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ `ericvh/devel`; full `regress` + latency jobs. | +| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Pushes use `v9fs/linux` @ **`main`** unless you override inputs. Manual default ref is **`main`**. | +| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ **`main`**; full `regress` + latency jobs. | Log artifact names (avoid collisions when jobs run in parallel): From 62c1070d5cdb8e5242ec70d9de143fcd7486b4f3 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 08:16:23 -0500 Subject: [PATCH 08/69] CI: fix permissions so logs upload as artifacts - Set umask 022 and chmod logs after v9fs-run-tests\n- Add workflow chmod step before upload-artifact as backstop\n- Update README wording for main branch Made-with: Cursor --- .github/workflows/demand.yml | 10 ++++++++++ .github/workflows/nightly.yml | 10 ++++++++++ README.md | 4 ++-- scripts/v9fs-run-tests | 7 +++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 454e2e7..08ba6d9 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -89,6 +89,11 @@ jobs: v9fs-test-env:local \ bash -lc "v9fs-run-tests short ci" + - name: Fix log permissions (for artifact upload) + if: always() + run: | + sudo chmod -R a+rX logs || true + - name: Store Logs uses: actions/upload-artifact@v4 with: @@ -129,6 +134,11 @@ jobs: v9fs-test-env:local \ bash -lc "v9fs-run-tests short latency && echo \"$RESULTSTAG $KERNEL_REPO $KERNEL_REF\" > /home/v9fs-test/test/logs/tag.txt" + - name: Fix log permissions (for artifact upload) + if: always() + run: | + sudo chmod -R a+rX logs || true + - name: Store Logs uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 15aab39..ae5042b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -68,6 +68,11 @@ jobs: v9fs-test-env:local \ bash -lc "v9fs-run-tests regress ci" + - name: Fix log permissions (for artifact upload) + if: always() + run: | + sudo chmod -R a+rX logs || true + - name: Store Logs uses: actions/upload-artifact@v4 with: @@ -103,6 +108,11 @@ jobs: v9fs-test-env:local \ bash -lc "v9fs-run-tests short latency && echo Nightly v9fs/linux main > /home/v9fs-test/test/logs/tag.txt" + - name: Fix log permissions (for artifact upload) + if: always() + run: | + sudo chmod -R a+rX logs || true + - name: Store Logs uses: actions/upload-artifact@v4 with: diff --git a/README.md b/README.md index 17f0dd0..40fa3e8 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,8 @@ CI uses the same Docker + QEMU flow as local development: | Workflow | File | When it runs | | ------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Pushes use `v9fs/linux` @ **`main`** unless you override inputs. Manual default ref is **`main`**. | -| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ **`main`**; full `regress` + latency jobs. | +| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Pushes use `v9fs/linux` @ `**main`** unless you override inputs. Manual default ref is `**main**`. | +| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ `**main`**; full `regress` + latency jobs. | Log artifact names (avoid collisions when jobs run in parallel): diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 5ea9a3d..53854e3 100644 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -5,6 +5,10 @@ tests="${1:-short}" type="${2:-ci}" check="${3:-1}" +# Ensure artifacts created by root inside Docker are readable by the +# GitHub Actions runner user when uploading `logs/` as an artifact. +umask 022 + # Default to the container-provided tools exposed to the guest via 9p. export TESTBIN="${TESTBIN:-/home/v9fs-test/testbin}" @@ -38,3 +42,6 @@ fi echo "QEMU run complete; logs at ${QEMULOG}" +# Make logs readable for artifact upload on the host runner. +chmod -R a+rX "logs/${TIMESTAMP}" 2>/dev/null || true + From bb79a53c9d8d5ed2534dcb2d830e08d6a2e8ea43 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 10:08:24 -0500 Subject: [PATCH 09/69] CI: publish built kernel images to GHCR - Package kernel Image outputs into tar.gz in build job\n- Push as OCI artifact to ghcr.io//v9fs-test-kernel via ORAS\n- Add packages:write permissions and document retrieval Made-with: Cursor --- .github/workflows/demand.yml | 26 ++++++++++++++++++++++++++ .github/workflows/nightly.yml | 26 ++++++++++++++++++++++++++ CHANGES.md | 1 + README.md | 18 ++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 08ba6d9..7518216 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -1,5 +1,9 @@ name: CI (push and manual) +permissions: + contents: read + packages: write + on: # Run CI on every push (all branches). Narrow with `branches:` if this is too noisy. push: @@ -61,6 +65,28 @@ jobs: path: kernel/.build/arch/*/boot/*Image retention-days: 7 + - name: Install ORAS + uses: oras-project/setup-oras@v1 + + - name: Package kernel image(s) for GHCR + run: | + set -euo pipefail + mkdir -p dist + tar -C kernel -czf "dist/kernel-image-${GITHUB_SHA}.tar.gz" .build/arch/*/boot/*Image + + - name: Publish kernel image package to GHCR (OCI artifact) + env: + GHCR_REPO: ghcr.io/${{ github.repository_owner }}/v9fs-test-kernel + ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" + run: | + set -euo pipefail + echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u "${{ github.actor }}" --password-stdin + + # Publish a content-addressed artifact tagged by commit SHA, plus a ref tag for convenience. + oras push "${GHCR_REPO}:${GITHUB_SHA}" \ + "dist/kernel-image-${GITHUB_SHA}.tar.gz:application/gzip" + oras tag "${GHCR_REPO}:${GITHUB_SHA}" "${KERNEL_REF}" + test: needs: build-kernel runs-on: ubuntu-latest diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ae5042b..ce6a43f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,5 +1,9 @@ name: Mainline +permissions: + contents: read + packages: write + on: schedule: - cron: '0 0 * * *' # Run every day at midnight @@ -40,6 +44,28 @@ jobs: path: kernel/.build/arch/*/boot/*Image retention-days: 7 + - name: Install ORAS + uses: oras-project/setup-oras@v1 + + - name: Package kernel image(s) for GHCR + run: | + set -euo pipefail + mkdir -p dist + tar -C kernel -czf "dist/kernel-image-${GITHUB_SHA}.tar.gz" .build/arch/*/boot/*Image + + - name: Publish kernel image package to GHCR (OCI artifact) + env: + GHCR_REPO: ghcr.io/${{ github.repository_owner }}/v9fs-test-kernel + ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" + run: | + set -euo pipefail + echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u "${{ github.actor }}" --password-stdin + + oras push "${GHCR_REPO}:${GITHUB_SHA}" \ + "dist/kernel-image-${GITHUB_SHA}.tar.gz:application/gzip" + oras tag "${GHCR_REPO}:${GITHUB_SHA}" "nightly" + oras tag "${GHCR_REPO}:${GITHUB_SHA}" "main" + test: needs: build-kernel runs-on: ubuntu-latest diff --git a/CHANGES.md b/CHANGES.md index 05f08b4..da93a23 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,4 +8,5 @@ - Ignore common local build outputs (`kernel/`, `tmp/`, generated initrd/pid files) in `.gitignore`. - **CI workflow** (`.github/workflows/demand.yml`): run on **every `push`** as well as **manual** `workflow_dispatch`; kernel checkout defaults to **`v9fs/linux` @ `main`** (stable; manual dispatch can still override `ref`). - **Fix Actions**: kernel `ref` pinned to **`main`** where defaults apply (push CI and nightly), avoiding volatile branches on `v9fs/linux`. +- Publish built kernel images to GitHub Packages (GHCR) as an OCI artifact (`ghcr.io/v9fs/v9fs-test-kernel`) tagged by commit SHA and `main`/`nightly`. diff --git a/README.md b/README.md index 40fa3e8..7a2b8fb 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,24 @@ CI uses the same Docker + QEMU flow as local development: | **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Pushes use `v9fs/linux` @ `**main`** unless you override inputs. Manual default ref is `**main**`. | | **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ `**main`**; full `regress` + latency jobs. | +### Kernel images as packages (GHCR) + +The kernel image(s) produced by the build step are also published to GitHub +Packages (GHCR) as an **OCI artifact** (in addition to the intra-workflow +`kernel-image` Action artifact used by the downstream jobs). + +- **Package**: `ghcr.io/v9fs/v9fs-test-kernel` +- **Tags**: + - Commit SHA (e.g. `:`) + - Convenience tag matching the kernel ref (e.g. `:main`) + +To fetch the latest `main` build: + +```bash +oras pull ghcr.io/v9fs/v9fs-test-kernel:main -o . +tar -tzf kernel-image-*.tar.gz | head +``` + Log artifact names (avoid collisions when jobs run in parallel): From 7476e8c927939339b6d3610f5dafdd66cf56647b Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 10:10:51 -0500 Subject: [PATCH 10/69] CI: surface guest test failures via exit code Run full test matrix in-guest, write logs//guest.exitcode, and have v9fs-run-tests exit nonzero when failures occurred so GitHub Actions shows red while still running everything. Made-with: Cursor --- scripts/v9fs-guest-run | 9 +++++++-- scripts/v9fs-run-tests | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/scripts/v9fs-guest-run b/scripts/v9fs-guest-run index 7113407..d1f23b0 100755 --- a/scripts/v9fs-guest-run +++ b/scripts/v9fs-guest-run @@ -23,6 +23,8 @@ echo "GUEST: TESTBIN=$TESTBIN" mkdir -p "$PATH_MNTDIR" rm -rf "$PATH_MNTDIR"/* +rc=0 + for f in "$repo"/fstabs-"$tests"/*; do ft="$(basename "$f")" mode="$(echo "$ft" | awk -F- '{print $1}')" @@ -57,6 +59,7 @@ for f in "$repo"/fstabs-"$tests"/*; do echo "SUCCESS" | tee -a "$results" else echo "FAIL" | tee -a "$results" + rc=1 fi done @@ -64,9 +67,11 @@ for f in "$repo"/fstabs-"$tests"/*; do if ! diff -b "$results" "$repo/good/$cfg/results.log" >/dev/null 2>&1; then echo "GUEST: Configuration $f FAILED" cat "$results" - exit 1 + rc=1 fi fi done -echo "GUEST: tests done" +echo "$rc" >"$repo/logs/$ts/guest.exitcode" 2>/dev/null || true +echo "GUEST: tests done rc=$rc" +exit "$rc" diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 53854e3..c5cacd2 100644 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -45,3 +45,17 @@ echo "QEMU run complete; logs at ${QEMULOG}" # Make logs readable for artifact upload on the host runner. chmod -R a+rX "logs/${TIMESTAMP}" 2>/dev/null || true +# The guest writes a status marker into the shared logs directory. +# Use it to fail CI while still running the whole suite. +guest_rc_file="logs/${TIMESTAMP}/guest.exitcode" +if [ -f "${guest_rc_file}" ]; then + guest_rc="$(cat "${guest_rc_file}" 2>/dev/null || echo 1)" + if [ "${guest_rc}" != "0" ]; then + echo "Guest reported failures (rc=${guest_rc})" + exit "${guest_rc}" + fi +else + echo "WARNING: missing ${guest_rc_file}; treating as failure" + exit 2 +fi + From b6457a2f6eed6c4cd5bcb7f46e96c0605543e4c0 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 11:32:01 -0500 Subject: [PATCH 11/69] CI: include bzImage in kernel artifacts and GHCR package GitHub runners build x86_64 kernels as arch/x86_64/boot/bzImage;\npackage/upload now include both *Image and bzImage and tar step is robust. Made-with: Cursor --- .github/workflows/demand.yml | 14 ++++++++++++-- .github/workflows/nightly.yml | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 7518216..6fefd10 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -62,7 +62,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: kernel-image - path: kernel/.build/arch/*/boot/*Image + path: | + kernel/.build/arch/*/boot/*Image + kernel/.build/arch/*/boot/bzImage retention-days: 7 - name: Install ORAS @@ -72,7 +74,15 @@ jobs: run: | set -euo pipefail mkdir -p dist - tar -C kernel -czf "dist/kernel-image-${GITHUB_SHA}.tar.gz" .build/arch/*/boot/*Image + mapfile -t files < <( + find kernel/.build/arch -type f \( -name '*Image' -o -name 'bzImage' \) -print | sort + ) + if [ "${#files[@]}" -eq 0 ]; then + echo "ERROR: no kernel images found under kernel/.build/arch" + find kernel -maxdepth 5 -type f | sed 's|^| |' + exit 2 + fi + tar -czf "dist/kernel-image-${GITHUB_SHA}.tar.gz" "${files[@]}" - name: Publish kernel image package to GHCR (OCI artifact) env: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ce6a43f..2d847d4 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -41,7 +41,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: kernel-image - path: kernel/.build/arch/*/boot/*Image + path: | + kernel/.build/arch/*/boot/*Image + kernel/.build/arch/*/boot/bzImage retention-days: 7 - name: Install ORAS @@ -51,7 +53,15 @@ jobs: run: | set -euo pipefail mkdir -p dist - tar -C kernel -czf "dist/kernel-image-${GITHUB_SHA}.tar.gz" .build/arch/*/boot/*Image + mapfile -t files < <( + find kernel/.build/arch -type f \( -name '*Image' -o -name 'bzImage' \) -print | sort + ) + if [ "${#files[@]}" -eq 0 ]; then + echo "ERROR: no kernel images found under kernel/.build/arch" + find kernel -maxdepth 5 -type f | sed 's|^| |' + exit 2 + fi + tar -czf "dist/kernel-image-${GITHUB_SHA}.tar.gz" "${files[@]}" - name: Publish kernel image package to GHCR (OCI artifact) env: From e00378c8324560b48ee43e49528added6ccca9c4 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 11:34:11 -0500 Subject: [PATCH 12/69] CI: default builds/tests on ARM64 runners Switch jobs to ubuntu-24.04-arm so kernel builds and QEMU guest tests default to arm64 outputs. Made-with: Cursor --- .github/workflows/demand.yml | 6 +++--- .github/workflows/nightly.yml | 6 +++--- CHANGES.md | 1 + README.md | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 6fefd10..8915669 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -26,7 +26,7 @@ on: workflow_call: jobs: build-kernel: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm env: # workflow_dispatch / workflow_call populate `inputs`; push does not — use defaults. KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} @@ -99,7 +99,7 @@ jobs: test: needs: build-kernel - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm steps: - name: Checkout test harness uses: actions/checkout@v4 @@ -139,7 +139,7 @@ jobs: latency: needs: build-kernel - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm env: RESULTSTAG: ${{ inputs.resultstag || github.sha }} KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2d847d4..357737e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: jobs: build-kernel: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm steps: - name: Checkout test harness uses: actions/checkout@v4 @@ -78,7 +78,7 @@ jobs: test: needs: build-kernel - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm steps: - name: Checkout test harness uses: actions/checkout@v4 @@ -118,7 +118,7 @@ jobs: latency: needs: build-kernel - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm steps: - name: Checkout test harness uses: actions/checkout@v4 diff --git a/CHANGES.md b/CHANGES.md index da93a23..c9dca15 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,4 +9,5 @@ - **CI workflow** (`.github/workflows/demand.yml`): run on **every `push`** as well as **manual** `workflow_dispatch`; kernel checkout defaults to **`v9fs/linux` @ `main`** (stable; manual dispatch can still override `ref`). - **Fix Actions**: kernel `ref` pinned to **`main`** where defaults apply (push CI and nightly), avoiding volatile branches on `v9fs/linux`. - Publish built kernel images to GitHub Packages (GHCR) as an OCI artifact (`ghcr.io/v9fs/v9fs-test-kernel`) tagged by commit SHA and `main`/`nightly`. +- Switch CI runners to ARM64 (`ubuntu-24.04-arm`) so kernel builds/tests default to arm64. diff --git a/README.md b/README.md index 7a2b8fb..0702d1e 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,8 @@ CI uses the same Docker + QEMU flow as local development: | Workflow | File | When it runs | | ------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Pushes use `v9fs/linux` @ `**main`** unless you override inputs. Manual default ref is `**main**`. | -| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ `**main`**; full `regress` + latency jobs. | +| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Runs on an **ARM64 runner** (`ubuntu-24.04-arm`) and defaults to `v9fs/linux` @ `**main`** unless you override inputs. | +| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ `**main`**; full `regress` + latency jobs on an **ARM64 runner** (`ubuntu-24.04-arm`). | ### Kernel images as packages (GHCR) From 6e414a3042b1cd34b4ba24a9d68c0f5e7a9f35cb Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 11:57:58 -0500 Subject: [PATCH 13/69] CI: propagate timestamp so guest exitcode is found Pass v9fs.ts= on kernel cmdline and have initrd export TIMESTAMP from it so guest writes guest.exitcode into the same logs// directory that the host checks. Made-with: Cursor --- scripts/v9fs-build-initrd | 4 ++++ scripts/v9fs-run-tests | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 7c406e9..b1dde54 100644 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -61,9 +61,13 @@ case "$cmdline" in tests="$(echo "$cmdline" | sed -n 's/.*v9fs.tests=\([^ ]*\).*/\1/p')" type="$(echo "$cmdline" | sed -n 's/.*v9fs.type=\([^ ]*\).*/\1/p')" check="$(echo "$cmdline" | sed -n 's/.*v9fs.check=\([^ ]*\).*/\1/p')" + ts="$(echo "$cmdline" | sed -n 's/.*v9fs.ts=\([^ ]*\).*/\1/p')" : "${tests:=smoke}" : "${type:=ci}" : "${check:=1}" + if [ -n "${ts:-}" ]; then + export TIMESTAMP="$ts" + fi echo "initrd: running guest tests tests=$tests type=$type check=$check" if [ -f /mnt/9/test/scripts/v9fs-guest-run ]; then diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index c5cacd2..acbfff8 100644 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -28,7 +28,7 @@ mountpoint -q /workspaces/share/testbin || mount --bind /home/v9fs-test/testbin export FSDEV_PATH="/workspaces/share" # Run the suite inside the guest and poweroff when complete. -export EXTRA_APPEND="v9fs.run=1 v9fs.tests=${tests} v9fs.type=${type} v9fs.check=${check}" +export EXTRA_APPEND="v9fs.run=1 v9fs.tests=${tests} v9fs.type=${type} v9fs.check=${check} v9fs.ts=${TIMESTAMP}" /home/v9fs-test/test/qemu.bash From 83b35a213dbca2d49b7f1e0c8762338c7dafde27 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 14:04:39 -0500 Subject: [PATCH 14/69] CI: publish arm64 Image as release asset Create/update kernel-main and kernel-nightly releases and upload arch/arm64/boot/Image as a stable asset so it can be fetched via wget. Made-with: Cursor --- .github/workflows/demand.yml | 22 +++++++++++++++++++++- .github/workflows/nightly.yml | 21 ++++++++++++++++++++- README.md | 8 ++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 8915669..b8ef9b9 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -1,7 +1,7 @@ name: CI (push and manual) permissions: - contents: read + contents: write packages: write on: @@ -67,6 +67,26 @@ jobs: kernel/.build/arch/*/boot/bzImage retention-days: 7 + - name: Publish arm64 Image as release asset (kernel-main) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: kernel-main + TITLE: kernel-main + run: | + set -euo pipefail + img="kernel/.build/arch/arm64/boot/Image" + if [ ! -f "$img" ]; then + echo "ERROR: missing $img" + find kernel/.build -maxdepth 6 -type f | sed 's|^| |' + exit 2 + fi + + # Create (or ensure) a stable tag-based release and replace the asset. + gh release view "$TAG" >/dev/null 2>&1 || \ + gh release create "$TAG" --title "$TITLE" --notes "Automated arm64 kernel Image from CI (${GITHUB_SHA})." --prerelease + cp -f "$img" Image + gh release upload "$TAG" Image --clobber + - name: Install ORAS uses: oras-project/setup-oras@v1 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 357737e..bd3c255 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,7 +1,7 @@ name: Mainline permissions: - contents: read + contents: write packages: write on: @@ -46,6 +46,25 @@ jobs: kernel/.build/arch/*/boot/bzImage retention-days: 7 + - name: Publish arm64 Image as release asset (kernel-nightly) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: kernel-nightly + TITLE: kernel-nightly + run: | + set -euo pipefail + img="kernel/.build/arch/arm64/boot/Image" + if [ ! -f "$img" ]; then + echo "ERROR: missing $img" + find kernel/.build -maxdepth 6 -type f | sed 's|^| |' + exit 2 + fi + + gh release view "$TAG" >/dev/null 2>&1 || \ + gh release create "$TAG" --title "$TITLE" --notes "Automated arm64 kernel Image from nightly (${GITHUB_SHA})." --prerelease + cp -f "$img" Image + gh release upload "$TAG" Image --clobber + - name: Install ORAS uses: oras-project/setup-oras@v1 diff --git a/README.md b/README.md index 0702d1e..40e7c22 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,14 @@ oras pull ghcr.io/v9fs/v9fs-test-kernel:main -o . tar -tzf kernel-image-*.tar.gz | head ``` +### Kernel images as direct downloads (GitHub Releases) + +CI also uploads the **arm64** kernel `Image` as a stable release asset so you can +download it with `wget`: + +- **CI / main**: `https://github.com/v9fs/test/releases/download/kernel-main/Image` +- **Nightly**: `https://github.com/v9fs/test/releases/download/kernel-nightly/Image` + Log artifact names (avoid collisions when jobs run in parallel): From 6bb9a636b65b7008e57a56c092aae206671cbe54 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 14:10:49 -0500 Subject: [PATCH 15/69] Fix guest benchmarks: ship binaries and libs; always upload logs - Copy dbench/postmark/fsx + client.txt into /home/v9fs-test/testbin (no host symlinks)\n- Include shared library deps for those tools in initrd\n- Ensure log artifacts upload even when tests fail Made-with: Cursor --- .github/workflows/demand.yml | 2 ++ .github/workflows/nightly.yml | 2 ++ Dockerfile | 8 ++++---- scripts/v9fs-build-initrd | 12 ++++++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index b8ef9b9..69225a9 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -151,6 +151,7 @@ jobs: sudo chmod -R a+rX logs || true - name: Store Logs + if: always() uses: actions/upload-artifact@v4 with: name: test-results-ci @@ -196,6 +197,7 @@ jobs: sudo chmod -R a+rX logs || true - name: Store Logs + if: always() uses: actions/upload-artifact@v4 with: name: test-results-latency diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index bd3c255..dafc365 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -129,6 +129,7 @@ jobs: sudo chmod -R a+rX logs || true - name: Store Logs + if: always() uses: actions/upload-artifact@v4 with: name: test-results-regression @@ -169,6 +170,7 @@ jobs: sudo chmod -R a+rX logs || true - name: Store Logs + if: always() uses: actions/upload-artifact@v4 with: name: test-results-latency diff --git a/Dockerfile b/Dockerfile index 707ad01..f43c0ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,10 +48,10 @@ RUN useradd -m -s /bin/bash v9fs-test \ # Provide a shared testbin layout compatible with existing scripts. # These paths are reachable from the guest via 9p (mounted at /mnt/9). RUN mkdir -p /home/v9fs-test/testbin/dbench /home/v9fs-test/testbin/postmark /home/v9fs-test/testbin/fsx \ - && ln -sf /usr/bin/dbench /home/v9fs-test/testbin/dbench/dbench \ - && (test -f /usr/share/dbench/client.txt && ln -sf /usr/share/dbench/client.txt /home/v9fs-test/testbin/dbench/client.txt || true) \ - && ln -sf /usr/bin/postmark /home/v9fs-test/testbin/postmark/postmark \ - && ln -sf /usr/local/bin/fsx /home/v9fs-test/testbin/fsx/fsx \ + && cp -f /usr/bin/dbench /home/v9fs-test/testbin/dbench/dbench \ + && cp -f /usr/share/dbench/client.txt /home/v9fs-test/testbin/dbench/client.txt \ + && cp -f /usr/bin/postmark /home/v9fs-test/testbin/postmark/postmark \ + && cp -f /usr/local/bin/fsx /home/v9fs-test/testbin/fsx/fsx \ && chown -R v9fs-test:v9fs-test /home/v9fs-test/testbin COPY scripts/ /usr/local/bin/ diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index b1dde54..375114c 100644 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -133,6 +133,18 @@ if [ -f /lib/ld-linux-aarch64.so.1 ]; then mkdir -p "$work/lib" cp -L /lib/ld-linux-aarch64.so.1 "$work/lib/ld-linux-aarch64.so.1" fi +if [ -f /lib64/ld-linux-x86-64.so.2 ]; then + mkdir -p "$work/lib64" + cp -L /lib64/ld-linux-x86-64.so.2 "$work/lib64/ld-linux-x86-64.so.2" +fi + +# Bench/test helpers are executed inside the initrd (from 9p), so the initrd +# must include their shared library dependencies. +for b in /usr/bin/dbench /usr/bin/postmark /usr/local/bin/fsx; do + if [ -x "$b" ]; then + copy_libs "$b" + fi +done (cd "$work" && find . -print0 | cpio --null -ov --format=newc) >"$out" From 87316d595de3f68bb8671bb3bbb1e003b7080012 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 15:12:53 -0500 Subject: [PATCH 16/69] CI: dump QEMU logs to job output on failure When v9fs-run-tests fails, print qemu.log and per-test logs into the CI stream to speed up debugging (artifacts still uploaded via always()). Made-with: Cursor --- .github/workflows/demand.yml | 44 +++++++++++++++++++++++++++++++++++ .github/workflows/nightly.yml | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 69225a9..6ee1736 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -145,6 +145,28 @@ jobs: v9fs-test-env:local \ bash -lc "v9fs-run-tests short ci" + - name: Dump logs on failure + if: failure() + run: | + set -euo pipefail + echo "==== logs tree ====" + (ls -R logs || true) + echo "==== qemu.log (tail) ====" + for f in logs/*/qemu.log; do + echo "---- $f ----" + tail -n 250 "$f" || true + done + echo "==== per-test logs (tail) ====" + for f in logs/*/*/*/*.log; do + echo "---- $f ----" + tail -n 80 "$f" || true + done + echo "==== guest exitcode markers ====" + for f in logs/*/guest.exitcode; do + echo "---- $f ----" + cat "$f" || true + done + - name: Fix log permissions (for artifact upload) if: always() run: | @@ -191,6 +213,28 @@ jobs: v9fs-test-env:local \ bash -lc "v9fs-run-tests short latency && echo \"$RESULTSTAG $KERNEL_REPO $KERNEL_REF\" > /home/v9fs-test/test/logs/tag.txt" + - name: Dump logs on failure + if: failure() + run: | + set -euo pipefail + echo "==== logs tree ====" + (ls -R logs || true) + echo "==== qemu.log (tail) ====" + for f in logs/*/qemu.log; do + echo "---- $f ----" + tail -n 250 "$f" || true + done + echo "==== per-test logs (tail) ====" + for f in logs/*/*/*/*.log; do + echo "---- $f ----" + tail -n 80 "$f" || true + done + echo "==== guest exitcode markers ====" + for f in logs/*/guest.exitcode; do + echo "---- $f ----" + cat "$f" || true + done + - name: Fix log permissions (for artifact upload) if: always() run: | diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index dafc365..ad9b88d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -123,6 +123,28 @@ jobs: v9fs-test-env:local \ bash -lc "v9fs-run-tests regress ci" + - name: Dump logs on failure + if: failure() + run: | + set -euo pipefail + echo "==== logs tree ====" + (ls -R logs || true) + echo "==== qemu.log (tail) ====" + for f in logs/*/qemu.log; do + echo "---- $f ----" + tail -n 250 "$f" || true + done + echo "==== per-test logs (tail) ====" + for f in logs/*/*/*/*.log; do + echo "---- $f ----" + tail -n 80 "$f" || true + done + echo "==== guest exitcode markers ====" + for f in logs/*/guest.exitcode; do + echo "---- $f ----" + cat "$f" || true + done + - name: Fix log permissions (for artifact upload) if: always() run: | @@ -164,6 +186,28 @@ jobs: v9fs-test-env:local \ bash -lc "v9fs-run-tests short latency && echo Nightly v9fs/linux main > /home/v9fs-test/test/logs/tag.txt" + - name: Dump logs on failure + if: failure() + run: | + set -euo pipefail + echo "==== logs tree ====" + (ls -R logs || true) + echo "==== qemu.log (tail) ====" + for f in logs/*/qemu.log; do + echo "---- $f ----" + tail -n 250 "$f" || true + done + echo "==== per-test logs (tail) ====" + for f in logs/*/*/*/*.log; do + echo "---- $f ----" + tail -n 80 "$f" || true + done + echo "==== guest exitcode markers ====" + for f in logs/*/guest.exitcode; do + echo "---- $f ----" + cat "$f" || true + done + - name: Fix log permissions (for artifact upload) if: always() run: | From 3d785276180239ca83f79feed47c348a0052f055 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 15:55:38 -0500 Subject: [PATCH 17/69] Fix CI flake: ensure guest.exitcode is flushed - sync after writing logs//guest.exitcode in guest\n- sync before initrd poweroff\n- host waits briefly for guest.exitcode to appear before failing Made-with: Cursor --- scripts/v9fs-build-initrd | 1 + scripts/v9fs-guest-run | 1 + scripts/v9fs-run-tests | 19 ++++++++++++------- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 375114c..5a1edec 100644 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -77,6 +77,7 @@ case "$cmdline" in ls -la /mnt/9/test/scripts 2>/dev/null || true ls -la /mnt/9/test 2>/dev/null || true fi + sync 2>/dev/null || true echo "initrd: powering off" poweroff -f || halt -f || reboot -f ;; diff --git a/scripts/v9fs-guest-run b/scripts/v9fs-guest-run index d1f23b0..9c125de 100755 --- a/scripts/v9fs-guest-run +++ b/scripts/v9fs-guest-run @@ -73,5 +73,6 @@ for f in "$repo"/fstabs-"$tests"/*; do done echo "$rc" >"$repo/logs/$ts/guest.exitcode" 2>/dev/null || true +sync 2>/dev/null || true echo "GUEST: tests done rc=$rc" exit "$rc" diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index acbfff8..77bad06 100644 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -48,14 +48,19 @@ chmod -R a+rX "logs/${TIMESTAMP}" 2>/dev/null || true # The guest writes a status marker into the shared logs directory. # Use it to fail CI while still running the whole suite. guest_rc_file="logs/${TIMESTAMP}/guest.exitcode" -if [ -f "${guest_rc_file}" ]; then - guest_rc="$(cat "${guest_rc_file}" 2>/dev/null || echo 1)" - if [ "${guest_rc}" != "0" ]; then - echo "Guest reported failures (rc=${guest_rc})" - exit "${guest_rc}" - fi -else +for _ in 1 2 3 4 5; do + [ -f "${guest_rc_file}" ] && break + sleep 1 +done + +if [ ! -f "${guest_rc_file}" ]; then echo "WARNING: missing ${guest_rc_file}; treating as failure" exit 2 fi +guest_rc="$(cat "${guest_rc_file}" 2>/dev/null || echo 1)" +if [ "${guest_rc}" != "0" ]; then + echo "Guest reported failures (rc=${guest_rc})" + exit "${guest_rc}" +fi + From 1d915325ba34a1e4a0aaf49ae2630158e1c82282 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 15:58:15 -0500 Subject: [PATCH 18/69] CI: cache kernel build by v9fs/linux commit via GHCR - Compute kernel source SHA and tag GHCR artifact as linux-\n- Build-kernel job tries oras pull first; skips rebuild on hit\n- Publish step tags both linux- and ref convenience tags Made-with: Cursor --- .github/workflows/demand.yml | 46 ++++++++++++++++++++++++++++------- .github/workflows/nightly.yml | 46 ++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index 6ee1736..bf8bfe1 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -32,6 +32,7 @@ jobs: KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} # Pin to main for stable CI (avoid volatile topic branches). KERNEL_REF: ${{ inputs.ref || 'main' }} + GHCR_REPO: ghcr.io/${{ github.repository_owner }}/v9fs-test-kernel steps: - name: Checkout test harness uses: actions/checkout@v4 @@ -43,10 +44,40 @@ jobs: ref: ${{ env.KERNEL_REF }} path: linux + - name: Install ORAS + uses: oras-project/setup-oras@v1 + + - name: Determine kernel source SHA + id: kernelsha + run: | + set -euo pipefail + sha="$(git -C linux rev-parse HEAD)" + echo "sha=$sha" >>"$GITHUB_OUTPUT" + echo "KERNEL_SHA=$sha" >>"$GITHUB_ENV" + echo "Kernel SHA: $sha" + + - name: Try fetch cached kernel from GHCR + id: kernelcache + env: + ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" + run: | + set -euo pipefail + mkdir -p dist-cache + if oras pull "${GHCR_REPO}:linux-${KERNEL_SHA}" -o dist-cache; then + echo "cache_hit=true" >>"$GITHUB_OUTPUT" + tar -xzf dist-cache/kernel-image.tar.gz -C . + echo "Cache hit for linux-${KERNEL_SHA}" + else + echo "cache_hit=false" >>"$GITHUB_OUTPUT" + echo "Cache miss for linux-${KERNEL_SHA}" + fi + - name: Build Docker environment + if: steps.kernelcache.outputs.cache_hit != 'true' run: docker build -t v9fs-test-env:local . - name: Build kernel (QEMU guest kernel image) + if: steps.kernelcache.outputs.cache_hit != 'true' run: | mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" docker run --rm --privileged \ @@ -87,9 +118,6 @@ jobs: cp -f "$img" Image gh release upload "$TAG" Image --clobber - - name: Install ORAS - uses: oras-project/setup-oras@v1 - - name: Package kernel image(s) for GHCR run: | set -euo pipefail @@ -102,20 +130,20 @@ jobs: find kernel -maxdepth 5 -type f | sed 's|^| |' exit 2 fi - tar -czf "dist/kernel-image-${GITHUB_SHA}.tar.gz" "${files[@]}" + tar -czf "dist/kernel-image.tar.gz" "${files[@]}" - name: Publish kernel image package to GHCR (OCI artifact) env: - GHCR_REPO: ghcr.io/${{ github.repository_owner }}/v9fs-test-kernel ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" run: | set -euo pipefail echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u "${{ github.actor }}" --password-stdin - # Publish a content-addressed artifact tagged by commit SHA, plus a ref tag for convenience. - oras push "${GHCR_REPO}:${GITHUB_SHA}" \ - "dist/kernel-image-${GITHUB_SHA}.tar.gz:application/gzip" - oras tag "${GHCR_REPO}:${GITHUB_SHA}" "${KERNEL_REF}" + # Tag by kernel source SHA so test-only commits can reuse it. + oras push "${GHCR_REPO}:linux-${KERNEL_SHA}" \ + "dist/kernel-image.tar.gz:application/gzip" + # Convenience tag for the tracked ref. + oras tag "${GHCR_REPO}:linux-${KERNEL_SHA}" "${KERNEL_REF}" test: needs: build-kernel diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ad9b88d..5044f4f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -11,6 +11,8 @@ on: jobs: build-kernel: runs-on: ubuntu-24.04-arm + env: + GHCR_REPO: ghcr.io/${{ github.repository_owner }}/v9fs-test-kernel steps: - name: Checkout test harness uses: actions/checkout@v4 @@ -22,10 +24,40 @@ jobs: ref: 'main' path: linux + - name: Install ORAS + uses: oras-project/setup-oras@v1 + + - name: Determine kernel source SHA + id: kernelsha + run: | + set -euo pipefail + sha="$(git -C linux rev-parse HEAD)" + echo "sha=$sha" >>"$GITHUB_OUTPUT" + echo "KERNEL_SHA=$sha" >>"$GITHUB_ENV" + echo "Kernel SHA: $sha" + + - name: Try fetch cached kernel from GHCR + id: kernelcache + env: + ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" + run: | + set -euo pipefail + mkdir -p dist-cache + if oras pull "${GHCR_REPO}:linux-${KERNEL_SHA}" -o dist-cache; then + echo "cache_hit=true" >>"$GITHUB_OUTPUT" + tar -xzf dist-cache/kernel-image.tar.gz -C . + echo "Cache hit for linux-${KERNEL_SHA}" + else + echo "cache_hit=false" >>"$GITHUB_OUTPUT" + echo "Cache miss for linux-${KERNEL_SHA}" + fi + - name: Build Docker environment + if: steps.kernelcache.outputs.cache_hit != 'true' run: docker build -t v9fs-test-env:local . - name: Build kernel (QEMU guest kernel image) + if: steps.kernelcache.outputs.cache_hit != 'true' run: | mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" docker run --rm --privileged \ @@ -65,9 +97,6 @@ jobs: cp -f "$img" Image gh release upload "$TAG" Image --clobber - - name: Install ORAS - uses: oras-project/setup-oras@v1 - - name: Package kernel image(s) for GHCR run: | set -euo pipefail @@ -80,20 +109,19 @@ jobs: find kernel -maxdepth 5 -type f | sed 's|^| |' exit 2 fi - tar -czf "dist/kernel-image-${GITHUB_SHA}.tar.gz" "${files[@]}" + tar -czf "dist/kernel-image.tar.gz" "${files[@]}" - name: Publish kernel image package to GHCR (OCI artifact) env: - GHCR_REPO: ghcr.io/${{ github.repository_owner }}/v9fs-test-kernel ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" run: | set -euo pipefail echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u "${{ github.actor }}" --password-stdin - oras push "${GHCR_REPO}:${GITHUB_SHA}" \ - "dist/kernel-image-${GITHUB_SHA}.tar.gz:application/gzip" - oras tag "${GHCR_REPO}:${GITHUB_SHA}" "nightly" - oras tag "${GHCR_REPO}:${GITHUB_SHA}" "main" + oras push "${GHCR_REPO}:linux-${KERNEL_SHA}" \ + "dist/kernel-image.tar.gz:application/gzip" + oras tag "${GHCR_REPO}:linux-${KERNEL_SHA}" "nightly" + oras tag "${GHCR_REPO}:linux-${KERNEL_SHA}" "main" test: needs: build-kernel From 7585d09d151d84b6f140ac02b7728ae47315b586 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Fri, 24 Apr 2026 17:49:20 -0500 Subject: [PATCH 19/69] Split kernel publish from harness CI workflows - Add linux-kernel-publish.yml (repository_dispatch + workflow_dispatch)\n- demand/nightly: download published Image from GitHub Releases; no in-repo kernel build\n- Document dispatch model and update CHANGES Made-with: Cursor --- .github/workflows/demand.yml | 180 ++++----------------- .github/workflows/linux-kernel-publish.yml | 153 ++++++++++++++++++ .github/workflows/nightly.yml | 153 +++--------------- CHANGES.md | 6 +- README.md | 47 +++--- 5 files changed, 239 insertions(+), 300 deletions(-) create mode 100644 .github/workflows/linux-kernel-publish.yml diff --git a/.github/workflows/demand.yml b/.github/workflows/demand.yml index bf8bfe1..067b0d6 100644 --- a/.github/workflows/demand.yml +++ b/.github/workflows/demand.yml @@ -1,162 +1,52 @@ name: CI (push and manual) permissions: - contents: write - packages: write + contents: read on: - # Run CI on every push (all branches). Narrow with `branches:` if this is too noisy. + # Run tests on every push (all branches). Kernel builds are published separately + # (see `linux-kernel-publish.yml` + `repository_dispatch` from v9fs/linux). push: workflow_dispatch: inputs: - repo: - description: 'repository path' + kernel_release: + description: 'GitHub release tag that provides the arm64 `Image` asset (e.g. kernel-main)' required: true - default: 'v9fs/linux' - type: string - ref: - description: 'repository reference' - required: true - default: 'main' + default: 'kernel-main' type: string resultstag: description: 'how to tag results' default: 'ci' type: string workflow_call: + inputs: + kernel_release: + description: 'GitHub release tag that provides the arm64 `Image` asset' + required: false + default: 'kernel-main' + type: string + resultstag: + description: 'Optional tag string written into latency logs' + required: false + default: '' + type: string + jobs: - build-kernel: + test: runs-on: ubuntu-24.04-arm env: - # workflow_dispatch / workflow_call populate `inputs`; push does not — use defaults. - KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} - # Pin to main for stable CI (avoid volatile topic branches). - KERNEL_REF: ${{ inputs.ref || 'main' }} - GHCR_REPO: ghcr.io/${{ github.repository_owner }}/v9fs-test-kernel + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-main' }} steps: - name: Checkout test harness uses: actions/checkout@v4 - - name: Checkout kernel under test - uses: actions/checkout@v4 - with: - repository: ${{ env.KERNEL_REPO }} - ref: ${{ env.KERNEL_REF }} - path: linux - - - name: Install ORAS - uses: oras-project/setup-oras@v1 - - - name: Determine kernel source SHA - id: kernelsha + - name: Download published kernel Image run: | set -euo pipefail - sha="$(git -C linux rev-parse HEAD)" - echo "sha=$sha" >>"$GITHUB_OUTPUT" - echo "KERNEL_SHA=$sha" >>"$GITHUB_ENV" - echo "Kernel SHA: $sha" - - - name: Try fetch cached kernel from GHCR - id: kernelcache - env: - ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" - run: | - set -euo pipefail - mkdir -p dist-cache - if oras pull "${GHCR_REPO}:linux-${KERNEL_SHA}" -o dist-cache; then - echo "cache_hit=true" >>"$GITHUB_OUTPUT" - tar -xzf dist-cache/kernel-image.tar.gz -C . - echo "Cache hit for linux-${KERNEL_SHA}" - else - echo "cache_hit=false" >>"$GITHUB_OUTPUT" - echo "Cache miss for linux-${KERNEL_SHA}" - fi - - - name: Build Docker environment - if: steps.kernelcache.outputs.cache_hit != 'true' - run: docker build -t v9fs-test-env:local . - - - name: Build kernel (QEMU guest kernel image) - if: steps.kernelcache.outputs.cache_hit != 'true' - run: | - mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" - docker run --rm --privileged \ - -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ - -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ - -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ - -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ - -w /home/v9fs-test/test \ - v9fs-test-env:local \ - bash -lc "v9fs-build-kernel && v9fs-export-kernel /workspaces/linux /workspaces/linux/.build /workspaces/kernel" - - - name: Upload kernel artifact - uses: actions/upload-artifact@v4 - with: - name: kernel-image - path: | - kernel/.build/arch/*/boot/*Image - kernel/.build/arch/*/boot/bzImage - retention-days: 7 - - - name: Publish arm64 Image as release asset (kernel-main) - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: kernel-main - TITLE: kernel-main - run: | - set -euo pipefail - img="kernel/.build/arch/arm64/boot/Image" - if [ ! -f "$img" ]; then - echo "ERROR: missing $img" - find kernel/.build -maxdepth 6 -type f | sed 's|^| |' - exit 2 - fi - - # Create (or ensure) a stable tag-based release and replace the asset. - gh release view "$TAG" >/dev/null 2>&1 || \ - gh release create "$TAG" --title "$TITLE" --notes "Automated arm64 kernel Image from CI (${GITHUB_SHA})." --prerelease - cp -f "$img" Image - gh release upload "$TAG" Image --clobber - - - name: Package kernel image(s) for GHCR - run: | - set -euo pipefail - mkdir -p dist - mapfile -t files < <( - find kernel/.build/arch -type f \( -name '*Image' -o -name 'bzImage' \) -print | sort - ) - if [ "${#files[@]}" -eq 0 ]; then - echo "ERROR: no kernel images found under kernel/.build/arch" - find kernel -maxdepth 5 -type f | sed 's|^| |' - exit 2 - fi - tar -czf "dist/kernel-image.tar.gz" "${files[@]}" - - - name: Publish kernel image package to GHCR (OCI artifact) - env: - ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" - run: | - set -euo pipefail - echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u "${{ github.actor }}" --password-stdin - - # Tag by kernel source SHA so test-only commits can reuse it. - oras push "${GHCR_REPO}:linux-${KERNEL_SHA}" \ - "dist/kernel-image.tar.gz:application/gzip" - # Convenience tag for the tracked ref. - oras tag "${GHCR_REPO}:linux-${KERNEL_SHA}" "${KERNEL_REF}" - - test: - needs: build-kernel - runs-on: ubuntu-24.04-arm - steps: - - name: Checkout test harness - uses: actions/checkout@v4 - - - name: Download kernel artifact - uses: actions/download-artifact@v4 - with: - name: kernel-image - path: kernel/.build/arch + mkdir -p kernel/.build/arch/arm64/boot + gh release download "${KERNEL_RELEASE}" -p Image -D kernel/.build/arch/arm64/boot --clobber + ls -la kernel/.build/arch/arm64/boot/Image - name: Build Docker environment run: docker build -t v9fs-test-env:local . @@ -209,22 +99,21 @@ jobs: retention-days: 7 latency: - needs: build-kernel runs-on: ubuntu-24.04-arm env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-main' }} RESULTSTAG: ${{ inputs.resultstag || github.sha }} - KERNEL_REPO: ${{ inputs.repo || 'v9fs/linux' }} - # Pin to main for stable CI (avoid volatile topic branches). - KERNEL_REF: ${{ inputs.ref || 'main' }} steps: - name: Checkout test harness uses: actions/checkout@v4 - - name: Download kernel artifact - uses: actions/download-artifact@v4 - with: - name: kernel-image - path: kernel/.build/arch + - name: Download published kernel Image + run: | + set -euo pipefail + mkdir -p kernel/.build/arch/arm64/boot + gh release download "${KERNEL_RELEASE}" -p Image -D kernel/.build/arch/arm64/boot --clobber + ls -la kernel/.build/arch/arm64/boot/Image - name: Build Docker environment run: docker build -t v9fs-test-env:local . @@ -239,7 +128,7 @@ jobs: -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-run-tests short latency && echo \"$RESULTSTAG $KERNEL_REPO $KERNEL_REF\" > /home/v9fs-test/test/logs/tag.txt" + bash -lc "v9fs-run-tests short latency && echo \"$RESULTSTAG ${KERNEL_RELEASE} v9fs/linux\" > /home/v9fs-test/test/logs/tag.txt" - name: Dump logs on failure if: failure() @@ -275,4 +164,3 @@ jobs: name: test-results-latency path: logs retention-days: 7 - diff --git a/.github/workflows/linux-kernel-publish.yml b/.github/workflows/linux-kernel-publish.yml new file mode 100644 index 0000000..d53cbbd --- /dev/null +++ b/.github/workflows/linux-kernel-publish.yml @@ -0,0 +1,153 @@ +# Publish an arm64 `Image` built from v9fs/linux to this repo's GitHub Releases (+ GHCR). +# +# This repo cannot subscribe to pushes on another repo. Use one of: +# - `repository_dispatch` from v9fs/linux (recommended), or +# - `workflow_dispatch` here for manual / automation. +# +# Example repository_dispatch (from v9fs/linux CI or a PAT with repo scope on v9fs/test): +# event_type: publish-kernel-image +# client_payload: +# linux_repository: v9fs/linux +# linux_ref: main +# release_tag: kernel-main +# release_title: kernel-main # optional + +name: Publish Linux kernel (arm64 Image) + +on: + repository_dispatch: + types: [publish-kernel-image] + workflow_dispatch: + inputs: + linux_repository: + description: 'Kernel GitHub repository (owner/name)' + required: true + default: 'v9fs/linux' + type: string + linux_ref: + description: 'Git ref to build (branch, tag, or SHA)' + required: true + default: 'main' + type: string + release_tag: + description: 'Release tag to publish/update (e.g. kernel-main, kernel-nightly, kernel-6.12.0)' + required: true + default: 'kernel-main' + type: string + release_title: + description: 'Optional GitHub release title (defaults to release_tag)' + required: false + default: '' + type: string + +permissions: + contents: write + packages: write + +jobs: + publish: + runs-on: ubuntu-24.04-arm + env: + GHCR_REPO: ghcr.io/${{ github.repository_owner }}/v9fs-test-kernel + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout test harness (Dockerfile + scripts) + uses: actions/checkout@v4 + + - name: Resolve parameters + id: params + run: | + set -euo pipefail + if [ "${GITHUB_EVENT_NAME}" = "repository_dispatch" ]; then + python3 -c "import json,os; ev=json.load(open(os.environ['GITHUB_EVENT_PATH'])); p=ev.get('client_payload') or {}; o=open(os.environ['GITHUB_OUTPUT'],'a',encoding='utf-8'); \ + o.write('linux_repository=%s\n'%(p.get('linux_repository') or 'v9fs/linux')); \ + o.write('linux_ref=%s\n'%(p.get('linux_ref') or 'main')); \ + o.write('release_tag=%s\n'%(p.get('release_tag') or 'kernel-main')); \ + o.write('release_title=%s\n'%(p.get('release_title') or p.get('release_tag') or 'kernel-main'))" + else + title="${{ inputs.release_title }}" + if [ -z "${title}" ]; then title="${{ inputs.release_tag }}"; fi + { + echo "linux_repository=${{ inputs.linux_repository }}" + echo "linux_ref=${{ inputs.linux_ref }}" + echo "release_tag=${{ inputs.release_tag }}" + echo "release_title=${title}" + } >>"$GITHUB_OUTPUT" + fi + + - name: Checkout kernel under build + uses: actions/checkout@v4 + with: + repository: ${{ steps.params.outputs.linux_repository }} + ref: ${{ steps.params.outputs.linux_ref }} + path: linux + + - name: Install ORAS + uses: oras-project/setup-oras@v1 + + - name: Determine kernel source SHA + id: kernelsha + run: | + set -euo pipefail + sha="$(git -C linux rev-parse HEAD)" + echo "sha=$sha" >>"$GITHUB_OUTPUT" + echo "KERNEL_SHA=$sha" >>"$GITHUB_ENV" + echo "Kernel SHA: $sha" + + - name: Build Docker environment + run: docker build -t v9fs-test-env:local . + + - name: Build kernel (QEMU guest kernel image) + run: | + mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" + docker run --rm --privileged \ + -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ + -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ + -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-build-kernel && v9fs-export-kernel /workspaces/linux /workspaces/linux/.build /workspaces/kernel" + + - name: Publish arm64 Image as release asset + env: + TAG: ${{ steps.params.outputs.release_tag }} + TITLE: ${{ steps.params.outputs.release_title }} + run: | + set -euo pipefail + img="kernel/.build/arch/arm64/boot/Image" + if [ ! -f "$img" ]; then + echo "ERROR: missing $img" + find kernel/.build -maxdepth 6 -type f | sed 's|^| |' + exit 2 + fi + + gh release view "$TAG" >/dev/null 2>&1 || \ + gh release create "$TAG" --title "$TITLE" --notes "Automated arm64 kernel Image from ${GITHUB_REPOSITORY}@${GITHUB_SHA} (linux ${KERNEL_SHA})." --prerelease + cp -f "$img" Image + gh release upload "$TAG" Image --clobber + + - name: Package kernel image(s) for GHCR + run: | + set -euo pipefail + mkdir -p dist + mapfile -t files < <( + find kernel/.build/arch -type f \( -name '*Image' -o -name 'bzImage' \) -print | sort + ) + if [ "${#files[@]}" -eq 0 ]; then + echo "ERROR: no kernel images found under kernel/.build/arch" + find kernel -maxdepth 5 -type f | sed 's|^| |' + exit 2 + fi + tar -czf "dist/kernel-image.tar.gz" "${files[@]}" + + - name: Publish kernel image package to GHCR (OCI artifact) + env: + ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" + run: | + set -euo pipefail + echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u "${{ github.actor }}" --password-stdin + + oras push "${GHCR_REPO}:linux-${KERNEL_SHA}" \ + "dist/kernel-image.tar.gz:application/gzip" + oras tag "${GHCR_REPO}:linux-${KERNEL_SHA}" "${{ steps.params.outputs.release_tag }}" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5044f4f..24d8aee 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,140 +1,35 @@ name: Mainline permissions: - contents: write - packages: write + contents: read on: schedule: - cron: '0 0 * * *' # Run every day at midnight workflow_dispatch: + inputs: + kernel_release: + description: 'GitHub release tag that provides the arm64 `Image` asset (default: kernel-nightly)' + required: true + default: 'kernel-nightly' + type: string + jobs: - build-kernel: + test: runs-on: ubuntu-24.04-arm env: - GHCR_REPO: ghcr.io/${{ github.repository_owner }}/v9fs-test-kernel + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-nightly' }} steps: - name: Checkout test harness uses: actions/checkout@v4 - - name: Checkout kernel under test - uses: actions/checkout@v4 - with: - repository: 'v9fs/linux' - ref: 'main' - path: linux - - - name: Install ORAS - uses: oras-project/setup-oras@v1 - - - name: Determine kernel source SHA - id: kernelsha - run: | - set -euo pipefail - sha="$(git -C linux rev-parse HEAD)" - echo "sha=$sha" >>"$GITHUB_OUTPUT" - echo "KERNEL_SHA=$sha" >>"$GITHUB_ENV" - echo "Kernel SHA: $sha" - - - name: Try fetch cached kernel from GHCR - id: kernelcache - env: - ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" - run: | - set -euo pipefail - mkdir -p dist-cache - if oras pull "${GHCR_REPO}:linux-${KERNEL_SHA}" -o dist-cache; then - echo "cache_hit=true" >>"$GITHUB_OUTPUT" - tar -xzf dist-cache/kernel-image.tar.gz -C . - echo "Cache hit for linux-${KERNEL_SHA}" - else - echo "cache_hit=false" >>"$GITHUB_OUTPUT" - echo "Cache miss for linux-${KERNEL_SHA}" - fi - - - name: Build Docker environment - if: steps.kernelcache.outputs.cache_hit != 'true' - run: docker build -t v9fs-test-env:local . - - - name: Build kernel (QEMU guest kernel image) - if: steps.kernelcache.outputs.cache_hit != 'true' - run: | - mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" - docker run --rm --privileged \ - -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ - -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ - -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ - -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ - -w /home/v9fs-test/test \ - v9fs-test-env:local \ - bash -lc "v9fs-build-kernel && v9fs-export-kernel /workspaces/linux /workspaces/linux/.build /workspaces/kernel" - - - name: Upload kernel artifact - uses: actions/upload-artifact@v4 - with: - name: kernel-image - path: | - kernel/.build/arch/*/boot/*Image - kernel/.build/arch/*/boot/bzImage - retention-days: 7 - - - name: Publish arm64 Image as release asset (kernel-nightly) - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: kernel-nightly - TITLE: kernel-nightly - run: | - set -euo pipefail - img="kernel/.build/arch/arm64/boot/Image" - if [ ! -f "$img" ]; then - echo "ERROR: missing $img" - find kernel/.build -maxdepth 6 -type f | sed 's|^| |' - exit 2 - fi - - gh release view "$TAG" >/dev/null 2>&1 || \ - gh release create "$TAG" --title "$TITLE" --notes "Automated arm64 kernel Image from nightly (${GITHUB_SHA})." --prerelease - cp -f "$img" Image - gh release upload "$TAG" Image --clobber - - - name: Package kernel image(s) for GHCR + - name: Download published kernel Image run: | set -euo pipefail - mkdir -p dist - mapfile -t files < <( - find kernel/.build/arch -type f \( -name '*Image' -o -name 'bzImage' \) -print | sort - ) - if [ "${#files[@]}" -eq 0 ]; then - echo "ERROR: no kernel images found under kernel/.build/arch" - find kernel -maxdepth 5 -type f | sed 's|^| |' - exit 2 - fi - tar -czf "dist/kernel-image.tar.gz" "${files[@]}" - - - name: Publish kernel image package to GHCR (OCI artifact) - env: - ORAS_EXPERIMENTAL_OCI_ARTIFACT: "1" - run: | - set -euo pipefail - echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u "${{ github.actor }}" --password-stdin - - oras push "${GHCR_REPO}:linux-${KERNEL_SHA}" \ - "dist/kernel-image.tar.gz:application/gzip" - oras tag "${GHCR_REPO}:linux-${KERNEL_SHA}" "nightly" - oras tag "${GHCR_REPO}:linux-${KERNEL_SHA}" "main" - - test: - needs: build-kernel - runs-on: ubuntu-24.04-arm - steps: - - name: Checkout test harness - uses: actions/checkout@v4 - - - name: Download kernel artifact - uses: actions/download-artifact@v4 - with: - name: kernel-image - path: kernel/.build/arch + mkdir -p kernel/.build/arch/arm64/boot + gh release download "${KERNEL_RELEASE}" -p Image -D kernel/.build/arch/arm64/boot --clobber + ls -la kernel/.build/arch/arm64/boot/Image - name: Build Docker environment run: docker build -t v9fs-test-env:local . @@ -187,17 +82,20 @@ jobs: retention-days: 7 latency: - needs: build-kernel runs-on: ubuntu-24.04-arm + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-nightly' }} steps: - name: Checkout test harness uses: actions/checkout@v4 - - name: Download kernel artifact - uses: actions/download-artifact@v4 - with: - name: kernel-image - path: kernel/.build/arch + - name: Download published kernel Image + run: | + set -euo pipefail + mkdir -p kernel/.build/arch/arm64/boot + gh release download "${KERNEL_RELEASE}" -p Image -D kernel/.build/arch/arm64/boot --clobber + ls -la kernel/.build/arch/arm64/boot/Image - name: Build Docker environment run: docker build -t v9fs-test-env:local . @@ -212,7 +110,7 @@ jobs: -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc "v9fs-run-tests short latency && echo Nightly v9fs/linux main > /home/v9fs-test/test/logs/tag.txt" + bash -lc "v9fs-run-tests short latency && echo \"Nightly ${KERNEL_RELEASE} v9fs/linux\" > /home/v9fs-test/test/logs/tag.txt" - name: Dump logs on failure if: failure() @@ -248,4 +146,3 @@ jobs: name: test-results-latency path: logs retention-days: 7 - diff --git a/CHANGES.md b/CHANGES.md index c9dca15..b192678 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,8 +6,6 @@ - Stabilize 9p exports for guest tests by bind-mounting the workspace/kernel/tmp/testbins under `/workspaces/share` (`scripts/v9fs-run-tests`). - Give nightly/on-demand log artifacts **unique names** so parallel jobs do not collide on `actions/upload-artifact`. - Ignore common local build outputs (`kernel/`, `tmp/`, generated initrd/pid files) in `.gitignore`. -- **CI workflow** (`.github/workflows/demand.yml`): run on **every `push`** as well as **manual** `workflow_dispatch`; kernel checkout defaults to **`v9fs/linux` @ `main`** (stable; manual dispatch can still override `ref`). -- **Fix Actions**: kernel `ref` pinned to **`main`** where defaults apply (push CI and nightly), avoiding volatile branches on `v9fs/linux`. -- Publish built kernel images to GitHub Packages (GHCR) as an OCI artifact (`ghcr.io/v9fs/v9fs-test-kernel`) tagged by commit SHA and `main`/`nightly`. -- Switch CI runners to ARM64 (`ubuntu-24.04-arm`) so kernel builds/tests default to arm64. +- **Split workflows**: add `.github/workflows/linux-kernel-publish.yml` to build/publish **`v9fs/linux`** arm64 `Image` on **`repository_dispatch`** (from linux) or **`workflow_dispatch`**, publishing to **GitHub Releases** (`kernel-main`, `kernel-nightly`, `kernel-`, …) plus **GHCR** (`linux-` + release tag). +- **Harness CI** (`.github/workflows/demand.yml`, `.github/workflows/nightly.yml`) no longer builds the kernel; they **`gh release download`** the published `Image` (defaults: `kernel-main` / `kernel-nightly`) and run QEMU tests on **ARM64** runners. diff --git a/README.md b/README.md index 40e7c22..37eae0f 100644 --- a/README.md +++ b/README.md @@ -81,46 +81,49 @@ The guest mounts the repo at `/mnt/9/test` (see `scripts/v9fs-guest-run`). ## GitHub Actions -CI uses the same Docker + QEMU flow as local development: +CI uses the same Docker + QEMU flow as local development, but **kernel builds are +published separately** from the harness tests: -1. Build the kernel in a container and upload `kernel-image` -2. Download that artifact into `kernel/.build/arch/...` for test jobs -3. Run `v9fs-run-tests ...` with `--privileged` so the harness can bind-mount a - stable 9p export root (`/workspaces/share`) +1. A dedicated workflow builds `v9fs/linux` and publishes the arm64 `Image` to + **GitHub Releases** (and GHCR). +2. The harness workflows download that published `Image` into `kernel/.build/arch/...` + and run `v9fs-run-tests ...` with `--privileged` so the harness can bind-mount a + stable 9p export root (`/workspaces/share`). ### Workflows -| Workflow | File | When it runs | -| ------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), on **workflow_dispatch** (pick kernel repo/ref in the UI), and when **called** via `workflow_call`. Runs on an **ARM64 runner** (`ubuntu-24.04-arm`) and defaults to `v9fs/linux` @ `**main`** unless you override inputs. | -| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule, **workflow_dispatch**, fixed `v9fs/linux` @ `**main`**; full `regress` + latency jobs on an **ARM64 runner** (`ubuntu-24.04-arm`). | +| Workflow | File | When it runs | +| -------- | ---- | ------------ | +| **Publish Linux kernel** | `.github/workflows/linux-kernel-publish.yml` | **`repository_dispatch`** from `v9fs/linux` (recommended) or **`workflow_dispatch`** here. Builds arm64 `Image`, uploads it to a release tag you choose (`kernel-main`, `kernel-nightly`, `kernel-`, …), and pushes a GHCR bundle tagged by the **linux commit SHA** plus your release tag. | +| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), **manual** dispatch, or **`workflow_call`**. Downloads `Image` from the **`kernel-main`** release by default (override via `kernel_release`). | +| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule + **manual** dispatch. Downloads `Image` from **`kernel-nightly`** by default (override via `kernel_release`). | + +Because GitHub Actions cannot natively “watch” another repository’s pushes, the +`v9fs/linux` repo should call `repository_dispatch` on `v9fs/test` when branches change +(see the header comment in `linux-kernel-publish.yml` for an example payload). ### Kernel images as packages (GHCR) -The kernel image(s) produced by the build step are also published to GitHub -Packages (GHCR) as an **OCI artifact** (in addition to the intra-workflow -`kernel-image` Action artifact used by the downstream jobs). +Published kernels are also pushed to GitHub Packages (GHCR) as an OCI artifact: - **Package**: `ghcr.io/v9fs/v9fs-test-kernel` - **Tags**: - - Commit SHA (e.g. `:`) - - Convenience tag matching the kernel ref (e.g. `:main`) - -To fetch the latest `main` build: + - `linux-` (immutable) + - The **release tag** you passed (e.g. `kernel-main`, `kernel-nightly`, `kernel-6.12.0`) ```bash -oras pull ghcr.io/v9fs/v9fs-test-kernel:main -o . -tar -tzf kernel-image-*.tar.gz | head +oras pull ghcr.io/v9fs/v9fs-test-kernel:linux- -o . +tar -tzf kernel-image.tar.gz | head ``` ### Kernel images as direct downloads (GitHub Releases) -CI also uploads the **arm64** kernel `Image` as a stable release asset so you can -download it with `wget`: +The publish workflow uploads the **arm64** kernel `Image` as a stable release asset: -- **CI / main**: `https://github.com/v9fs/test/releases/download/kernel-main/Image` -- **Nightly**: `https://github.com/v9fs/test/releases/download/kernel-nightly/Image` +- **Rolling mainline**: `https://github.com/v9fs/test/releases/download/kernel-main/Image` +- **Rolling nightly**: `https://github.com/v9fs/test/releases/download/kernel-nightly/Image` +- **Versioned** (example): `https://github.com/v9fs/test/releases/download/kernel-6.12.0/Image` Log artifact names (avoid collisions when jobs run in parallel): From 09b4bc0a07bbb6b4ff87ee83eb5141ac56526a77 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:23:24 -0500 Subject: [PATCH 20/69] Archive rework take 1 under old/ and reset root Tag rework-take-1 and move all existing tracked files under old/rework-take-1/ for reference. Add minimal top-level docs + gitignore for clean-slate rebuild. Made-with: Cursor --- .gitignore | 2 +- CHANGES.md | 9 +- README.md | 9 ++ TODO.md | 4 + .../.github}/workflows/demand.yml | 0 .../workflows/linux-kernel-publish.yml | 0 .../.github}/workflows/nightly.yml | 0 old/rework-take-1/.gitignore | 5 + old/rework-take-1/CHANGES.md | 11 ++ Dockerfile => old/rework-take-1/Dockerfile | 0 LICENSE => old/rework-take-1/LICENSE | 0 old/rework-take-1/README.md | 133 ++++++++++++++++++ old/rework-take-1/TODO.md | 4 + .../rework-take-1/fstabs-regress}/virtio-mmap | 0 .../fstabs-regress}/virtio-nocache | 0 .../rework-take-1/fstabs-regress}/virtio-ra | 0 .../fstabs-short-old}/qemu-loose | 0 .../rework-take-1/fstabs-short-old}/qemu-mmap | 0 .../fstabs-short-old}/virtio-fscache | 0 .../fstabs-short-old}/virtio-loose | 0 .../fstabs-short-old}/virtio-mmap | 0 .../fstabs-short-old}/virtio-nocache | 0 .../rework-take-1/fstabs-short}/qemu-loose | 0 .../rework-take-1/fstabs-short}/qemu-mmap | 0 .../fstabs-short}/virtio-fscache | 0 .../rework-take-1/fstabs-short}/virtio-loose | 0 .../rework-take-1/fstabs-short}/virtio-mmap | 0 .../fstabs-short}/virtio-nocache | 0 .../rework-take-1/fstabs-short}/virtio-ra | 0 .../fstabs-smoke}/virtio-fscache | 0 .../rework-take-1/good}/fscache/dbench.log | 0 .../rework-take-1/good}/fscache/dbench.time | 0 .../rework-take-1/good}/fscache/fsx-mmap.log | 0 .../rework-take-1/good}/fscache/fsx-mmap.time | 0 .../rework-take-1/good}/fscache/fsx.log | 0 .../rework-take-1/good}/fscache/fsx.time | 0 .../rework-take-1/good}/fscache/ldconfig.log | 0 .../rework-take-1/good}/fscache/ldconfig.time | 0 .../rework-take-1/good}/fscache/postmark.log | 0 .../rework-take-1/good}/fscache/postmark.time | 0 .../rework-take-1/good}/fscache/results.log | 0 .../rework-take-1/good}/loose/dbench.log | 0 .../rework-take-1/good}/loose/dbench.time | 0 .../rework-take-1/good}/loose/fsx-mmap.log | 0 .../rework-take-1/good}/loose/fsx-mmap.time | 0 .../rework-take-1/good}/loose/fsx.log | 0 .../rework-take-1/good}/loose/fsx.time | 0 .../rework-take-1/good}/loose/ldconfig.log | 0 .../rework-take-1/good}/loose/ldconfig.time | 0 .../rework-take-1/good}/loose/postmark.log | 0 .../rework-take-1/good}/loose/postmark.time | 0 .../rework-take-1/good}/loose/results.log | 0 .../rework-take-1/good}/mmap/dbench.log | 0 .../rework-take-1/good}/mmap/dbench.time | 0 .../rework-take-1/good}/mmap/fsx-mmap.log | 0 .../rework-take-1/good}/mmap/fsx-mmap.time | 0 {good => old/rework-take-1/good}/mmap/fsx.log | 0 .../rework-take-1/good}/mmap/fsx.time | 0 .../rework-take-1/good}/mmap/ldconfig.log | 0 .../rework-take-1/good}/mmap/ldconfig.time | 0 .../rework-take-1/good}/mmap/postmark.log | 0 .../rework-take-1/good}/mmap/postmark.time | 0 .../rework-take-1/good}/mmap/results.log | 0 .../rework-take-1/good}/nocache/dbench.log | 0 .../rework-take-1/good}/nocache/dbench.time | 0 .../rework-take-1/good}/nocache/fsx-mmap.log | 0 .../rework-take-1/good}/nocache/fsx-mmap.time | 0 .../rework-take-1/good}/nocache/fsx.log | 0 .../rework-take-1/good}/nocache/fsx.time | 0 .../rework-take-1/good}/nocache/ldconfig.log | 0 .../rework-take-1/good}/nocache/ldconfig.time | 0 .../rework-take-1/good}/nocache/postmark.log | 0 .../rework-take-1/good}/nocache/postmark.time | 0 .../rework-take-1/good}/nocache/results.log | 0 .../rework-take-1/good}/ra/dbench.log | 0 .../rework-take-1/good}/ra/dbench.time | 0 .../rework-take-1/good}/ra/fsx-mmap.log | 0 .../rework-take-1/good}/ra/fsx-mmap.time | 0 {good => old/rework-take-1/good}/ra/fsx.log | 0 {good => old/rework-take-1/good}/ra/fsx.time | 0 .../rework-take-1/good}/ra/ldconfig.log | 0 .../rework-take-1/good}/ra/ldconfig.time | 0 .../rework-take-1/good}/ra/postmark.log | 0 .../rework-take-1/good}/ra/postmark.time | 0 .../rework-take-1/good}/ra/results.log | 0 .../rework-take-1/parser}/dbench.py | 0 .../rework-take-1/qemu-new.bash | 0 .../rework-take-1/qemu-old.bash | 0 qemu.bash => old/rework-take-1/qemu.bash | 0 {scripts => old/rework-take-1/scripts}/cpu | 0 .../rework-take-1/scripts}/v9fs-build-initrd | 0 .../rework-take-1/scripts}/v9fs-build-kernel | 0 .../rework-take-1/scripts}/v9fs-entrypoint | 0 .../rework-take-1/scripts}/v9fs-export-kernel | 0 .../rework-take-1/scripts}/v9fs-guest-run | 0 .../rework-take-1/scripts}/v9fs-run-tests | 0 test.bash => old/rework-take-1/test.bash | 0 .../rework-take-1/tests}/ci/dbench.bash | 0 .../rework-take-1/tests}/ci/fsx-mmap.bash | 0 .../rework-take-1/tests}/ci/fsx.bash | 0 .../rework-take-1/tests}/ci/ldconfig.bash | 0 .../rework-take-1/tests}/ci/postmark.bash | 0 .../rework-take-1/tests}/latency/dbench.bash | 0 103 files changed, 168 insertions(+), 9 deletions(-) rename {.github => old/rework-take-1/.github}/workflows/demand.yml (100%) rename {.github => old/rework-take-1/.github}/workflows/linux-kernel-publish.yml (100%) rename {.github => old/rework-take-1/.github}/workflows/nightly.yml (100%) create mode 100644 old/rework-take-1/.gitignore create mode 100644 old/rework-take-1/CHANGES.md rename Dockerfile => old/rework-take-1/Dockerfile (100%) rename LICENSE => old/rework-take-1/LICENSE (100%) create mode 100644 old/rework-take-1/README.md create mode 100644 old/rework-take-1/TODO.md rename {fstabs-regress => old/rework-take-1/fstabs-regress}/virtio-mmap (100%) rename {fstabs-regress => old/rework-take-1/fstabs-regress}/virtio-nocache (100%) rename {fstabs-regress => old/rework-take-1/fstabs-regress}/virtio-ra (100%) rename {fstabs-short-old => old/rework-take-1/fstabs-short-old}/qemu-loose (100%) rename {fstabs-short-old => old/rework-take-1/fstabs-short-old}/qemu-mmap (100%) rename {fstabs-short-old => old/rework-take-1/fstabs-short-old}/virtio-fscache (100%) rename {fstabs-short-old => old/rework-take-1/fstabs-short-old}/virtio-loose (100%) rename {fstabs-short-old => old/rework-take-1/fstabs-short-old}/virtio-mmap (100%) rename {fstabs-short-old => old/rework-take-1/fstabs-short-old}/virtio-nocache (100%) rename {fstabs-short => old/rework-take-1/fstabs-short}/qemu-loose (100%) rename {fstabs-short => old/rework-take-1/fstabs-short}/qemu-mmap (100%) rename {fstabs-short => old/rework-take-1/fstabs-short}/virtio-fscache (100%) rename {fstabs-short => old/rework-take-1/fstabs-short}/virtio-loose (100%) rename {fstabs-short => old/rework-take-1/fstabs-short}/virtio-mmap (100%) rename {fstabs-short => old/rework-take-1/fstabs-short}/virtio-nocache (100%) rename {fstabs-short => old/rework-take-1/fstabs-short}/virtio-ra (100%) rename {fstabs-smoke => old/rework-take-1/fstabs-smoke}/virtio-fscache (100%) rename {good => old/rework-take-1/good}/fscache/dbench.log (100%) rename {good => old/rework-take-1/good}/fscache/dbench.time (100%) rename {good => old/rework-take-1/good}/fscache/fsx-mmap.log (100%) rename {good => old/rework-take-1/good}/fscache/fsx-mmap.time (100%) rename {good => old/rework-take-1/good}/fscache/fsx.log (100%) rename {good => old/rework-take-1/good}/fscache/fsx.time (100%) rename {good => old/rework-take-1/good}/fscache/ldconfig.log (100%) rename {good => old/rework-take-1/good}/fscache/ldconfig.time (100%) rename {good => old/rework-take-1/good}/fscache/postmark.log (100%) rename {good => old/rework-take-1/good}/fscache/postmark.time (100%) rename {good => old/rework-take-1/good}/fscache/results.log (100%) rename {good => old/rework-take-1/good}/loose/dbench.log (100%) rename {good => old/rework-take-1/good}/loose/dbench.time (100%) rename {good => old/rework-take-1/good}/loose/fsx-mmap.log (100%) rename {good => old/rework-take-1/good}/loose/fsx-mmap.time (100%) rename {good => old/rework-take-1/good}/loose/fsx.log (100%) rename {good => old/rework-take-1/good}/loose/fsx.time (100%) rename {good => old/rework-take-1/good}/loose/ldconfig.log (100%) rename {good => old/rework-take-1/good}/loose/ldconfig.time (100%) rename {good => old/rework-take-1/good}/loose/postmark.log (100%) rename {good => old/rework-take-1/good}/loose/postmark.time (100%) rename {good => old/rework-take-1/good}/loose/results.log (100%) rename {good => old/rework-take-1/good}/mmap/dbench.log (100%) rename {good => old/rework-take-1/good}/mmap/dbench.time (100%) rename {good => old/rework-take-1/good}/mmap/fsx-mmap.log (100%) rename {good => old/rework-take-1/good}/mmap/fsx-mmap.time (100%) rename {good => old/rework-take-1/good}/mmap/fsx.log (100%) rename {good => old/rework-take-1/good}/mmap/fsx.time (100%) rename {good => old/rework-take-1/good}/mmap/ldconfig.log (100%) rename {good => old/rework-take-1/good}/mmap/ldconfig.time (100%) rename {good => old/rework-take-1/good}/mmap/postmark.log (100%) rename {good => old/rework-take-1/good}/mmap/postmark.time (100%) rename {good => old/rework-take-1/good}/mmap/results.log (100%) rename {good => old/rework-take-1/good}/nocache/dbench.log (100%) rename {good => old/rework-take-1/good}/nocache/dbench.time (100%) rename {good => old/rework-take-1/good}/nocache/fsx-mmap.log (100%) rename {good => old/rework-take-1/good}/nocache/fsx-mmap.time (100%) rename {good => old/rework-take-1/good}/nocache/fsx.log (100%) rename {good => old/rework-take-1/good}/nocache/fsx.time (100%) rename {good => old/rework-take-1/good}/nocache/ldconfig.log (100%) rename {good => old/rework-take-1/good}/nocache/ldconfig.time (100%) rename {good => old/rework-take-1/good}/nocache/postmark.log (100%) rename {good => old/rework-take-1/good}/nocache/postmark.time (100%) rename {good => old/rework-take-1/good}/nocache/results.log (100%) rename {good => old/rework-take-1/good}/ra/dbench.log (100%) rename {good => old/rework-take-1/good}/ra/dbench.time (100%) rename {good => old/rework-take-1/good}/ra/fsx-mmap.log (100%) rename {good => old/rework-take-1/good}/ra/fsx-mmap.time (100%) rename {good => old/rework-take-1/good}/ra/fsx.log (100%) rename {good => old/rework-take-1/good}/ra/fsx.time (100%) rename {good => old/rework-take-1/good}/ra/ldconfig.log (100%) rename {good => old/rework-take-1/good}/ra/ldconfig.time (100%) rename {good => old/rework-take-1/good}/ra/postmark.log (100%) rename {good => old/rework-take-1/good}/ra/postmark.time (100%) rename {good => old/rework-take-1/good}/ra/results.log (100%) rename {parser => old/rework-take-1/parser}/dbench.py (100%) rename qemu-new.bash => old/rework-take-1/qemu-new.bash (100%) rename qemu-old.bash => old/rework-take-1/qemu-old.bash (100%) rename qemu.bash => old/rework-take-1/qemu.bash (100%) rename {scripts => old/rework-take-1/scripts}/cpu (100%) rename {scripts => old/rework-take-1/scripts}/v9fs-build-initrd (100%) rename {scripts => old/rework-take-1/scripts}/v9fs-build-kernel (100%) rename {scripts => old/rework-take-1/scripts}/v9fs-entrypoint (100%) rename {scripts => old/rework-take-1/scripts}/v9fs-export-kernel (100%) rename {scripts => old/rework-take-1/scripts}/v9fs-guest-run (100%) rename {scripts => old/rework-take-1/scripts}/v9fs-run-tests (100%) rename test.bash => old/rework-take-1/test.bash (100%) rename {tests => old/rework-take-1/tests}/ci/dbench.bash (100%) rename {tests => old/rework-take-1/tests}/ci/fsx-mmap.bash (100%) rename {tests => old/rework-take-1/tests}/ci/fsx.bash (100%) rename {tests => old/rework-take-1/tests}/ci/ldconfig.bash (100%) rename {tests => old/rework-take-1/tests}/ci/postmark.bash (100%) rename {tests => old/rework-take-1/tests}/latency/dbench.bash (100%) diff --git a/.gitignore b/.gitignore index e2907e3..7013148 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -logs/* +logs/ kernel/ tmp/ initrd.cpio diff --git a/CHANGES.md b/CHANGES.md index b192678..82688c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,4 @@ ## Unreleased -- Start `rework` branch workflow and add baseline project docs (`README.md`, `CHANGES.md`, `TODO.md`). -- Add Docker + QEMU environment for local macOS runs and align GitHub Actions to use it. -- Run the suite **inside the QEMU guest** via initrd cmdline flags (`v9fs.run=1`, `v9fs.tests`, …) and a small guest runner (`scripts/v9fs-guest-run`), avoiding SSH/host port forwarding. -- Stabilize 9p exports for guest tests by bind-mounting the workspace/kernel/tmp/testbins under `/workspaces/share` (`scripts/v9fs-run-tests`). -- Give nightly/on-demand log artifacts **unique names** so parallel jobs do not collide on `actions/upload-artifact`. -- Ignore common local build outputs (`kernel/`, `tmp/`, generated initrd/pid files) in `.gitignore`. -- **Split workflows**: add `.github/workflows/linux-kernel-publish.yml` to build/publish **`v9fs/linux`** arm64 `Image` on **`repository_dispatch`** (from linux) or **`workflow_dispatch`**, publishing to **GitHub Releases** (`kernel-main`, `kernel-nightly`, `kernel-`, …) plus **GHCR** (`linux-` + release tag). -- **Harness CI** (`.github/workflows/demand.yml`, `.github/workflows/nightly.yml`) no longer builds the kernel; they **`gh release download`** the published `Image` (defaults: `kernel-main` / `kernel-nightly`) and run QEMU tests on **ARM64** runners. +- Clean-slate rework started; prior implementation preserved under `old/rework-take-1/` (tag `rework-take-1`). diff --git a/README.md b/README.md index 37eae0f..f3e51da 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +# v9fs test harness (clean-slate) + +This branch is being reworked from a clean slate. + +The previous implementation snapshot is preserved at: + +- Git tag: `rework-take-1` +- Directory: `old/rework-take-1/` + # v9fs test harness This repository contains **test code and scripts** for exercising the Linux **9p (v9fs)** filesystem. diff --git a/TODO.md b/TODO.md index e4ad804..d673d5c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,8 @@ ## TODO +- Rebuild the harness from scratch (see `old/rework-take-1/` for reference). + +## TODO + - Decide whether to keep `ubuntu-latest` runners or switch back to self-hosted for KVM acceleration. - Remove or clearly fence legacy SSH-based helpers (`test.bash`, `scripts/cpu`) if they are no longer part of the supported workflow. \ No newline at end of file diff --git a/.github/workflows/demand.yml b/old/rework-take-1/.github/workflows/demand.yml similarity index 100% rename from .github/workflows/demand.yml rename to old/rework-take-1/.github/workflows/demand.yml diff --git a/.github/workflows/linux-kernel-publish.yml b/old/rework-take-1/.github/workflows/linux-kernel-publish.yml similarity index 100% rename from .github/workflows/linux-kernel-publish.yml rename to old/rework-take-1/.github/workflows/linux-kernel-publish.yml diff --git a/.github/workflows/nightly.yml b/old/rework-take-1/.github/workflows/nightly.yml similarity index 100% rename from .github/workflows/nightly.yml rename to old/rework-take-1/.github/workflows/nightly.yml diff --git a/old/rework-take-1/.gitignore b/old/rework-take-1/.gitignore new file mode 100644 index 0000000..e2907e3 --- /dev/null +++ b/old/rework-take-1/.gitignore @@ -0,0 +1,5 @@ +logs/* +kernel/ +tmp/ +initrd.cpio +qemu.pid diff --git a/old/rework-take-1/CHANGES.md b/old/rework-take-1/CHANGES.md new file mode 100644 index 0000000..b192678 --- /dev/null +++ b/old/rework-take-1/CHANGES.md @@ -0,0 +1,11 @@ +## Unreleased + +- Start `rework` branch workflow and add baseline project docs (`README.md`, `CHANGES.md`, `TODO.md`). +- Add Docker + QEMU environment for local macOS runs and align GitHub Actions to use it. +- Run the suite **inside the QEMU guest** via initrd cmdline flags (`v9fs.run=1`, `v9fs.tests`, …) and a small guest runner (`scripts/v9fs-guest-run`), avoiding SSH/host port forwarding. +- Stabilize 9p exports for guest tests by bind-mounting the workspace/kernel/tmp/testbins under `/workspaces/share` (`scripts/v9fs-run-tests`). +- Give nightly/on-demand log artifacts **unique names** so parallel jobs do not collide on `actions/upload-artifact`. +- Ignore common local build outputs (`kernel/`, `tmp/`, generated initrd/pid files) in `.gitignore`. +- **Split workflows**: add `.github/workflows/linux-kernel-publish.yml` to build/publish **`v9fs/linux`** arm64 `Image` on **`repository_dispatch`** (from linux) or **`workflow_dispatch`**, publishing to **GitHub Releases** (`kernel-main`, `kernel-nightly`, `kernel-`, …) plus **GHCR** (`linux-` + release tag). +- **Harness CI** (`.github/workflows/demand.yml`, `.github/workflows/nightly.yml`) no longer builds the kernel; they **`gh release download`** the published `Image` (defaults: `kernel-main` / `kernel-nightly`) and run QEMU tests on **ARM64** runners. + diff --git a/Dockerfile b/old/rework-take-1/Dockerfile similarity index 100% rename from Dockerfile rename to old/rework-take-1/Dockerfile diff --git a/LICENSE b/old/rework-take-1/LICENSE similarity index 100% rename from LICENSE rename to old/rework-take-1/LICENSE diff --git a/old/rework-take-1/README.md b/old/rework-take-1/README.md new file mode 100644 index 0000000..37eae0f --- /dev/null +++ b/old/rework-take-1/README.md @@ -0,0 +1,133 @@ +# v9fs test harness + +This repository contains **test code and scripts** for exercising the Linux **9p (v9fs)** filesystem. + +The kernel source under test lives in the upstream repository: + +- `https://github.com/v9fs/linux` + +## What lives here + +- **CI workflows**: automation to build/run tests against a chosen kernel revision +- **Test code**: focused repros and regression tests for v9fs behavior +- **Scripts**: helpers to run locally and/or in CI + +## How we work in this repo + +- **Development branch**: all active work happens on `rework` +- **Change log**: keep `CHANGES.md` updated for every change set +- **Work tracking**: keep `TODO.md` updated as items are added/removed + +## Quick start + +This repo intentionally does *not* vendor the kernel source. Most workflows/scripts will: + +1. Fetch `github.com/v9fs/linux` (or a fork/branch you specify) +2. Build the kernel (or use a provided artifact) +3. Run the tests in this repository against that kernel + +### Local (macOS) via Docker + QEMU + +Clone the kernel repo beside this repo: + +```bash +git clone https://github.com/v9fs/linux ../linux +``` + +Build the test environment image: + +```bash +docker build -t v9fs-test-env:local . +``` + +Build the kernel once (and export it as a reusable artifact): + +```bash +mkdir -p ./tmp ./kernel +docker run --rm --privileged \ + -v "$PWD:/home/v9fs-test/test" \ + -v "$PWD/../linux:/workspaces/linux" \ + -v "$PWD/kernel:/workspaces/kernel" \ + -v "$PWD/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-build-kernel && v9fs-export-kernel /workspaces/linux /workspaces/linux/.build /workspaces/kernel" +``` + +Then run tests repeatedly without rebuilding the kernel: + +```bash +mkdir -p ./tmp +docker run --rm --privileged \ + -e KERNELBUILD=/workspaces/kernel/.build \ + -v "$PWD:/home/v9fs-test/test" \ + -v "$PWD/kernel:/workspaces/kernel" \ + -v "$PWD/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-run-tests short ci" +``` + +## How tests run (guest-direct) + +`v9fs-run-tests` boots QEMU with an initrd that mounts the host-exported workspace +over **9p** and then runs the suite **inside the guest** (no SSH/port-forwarding). + +The host-visible output is primarily the QEMU serial log: + +- `logs//qemu.log` + +The guest mounts the repo at `/mnt/9/test` (see `scripts/v9fs-guest-run`). + +## GitHub Actions + +CI uses the same Docker + QEMU flow as local development, but **kernel builds are +published separately** from the harness tests: + +1. A dedicated workflow builds `v9fs/linux` and publishes the arm64 `Image` to + **GitHub Releases** (and GHCR). +2. The harness workflows download that published `Image` into `kernel/.build/arch/...` + and run `v9fs-run-tests ...` with `--privileged` so the harness can bind-mount a + stable 9p export root (`/workspaces/share`). + +### Workflows + + +| Workflow | File | When it runs | +| -------- | ---- | ------------ | +| **Publish Linux kernel** | `.github/workflows/linux-kernel-publish.yml` | **`repository_dispatch`** from `v9fs/linux` (recommended) or **`workflow_dispatch`** here. Builds arm64 `Image`, uploads it to a release tag you choose (`kernel-main`, `kernel-nightly`, `kernel-`, …), and pushes a GHCR bundle tagged by the **linux commit SHA** plus your release tag. | +| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), **manual** dispatch, or **`workflow_call`**. Downloads `Image` from the **`kernel-main`** release by default (override via `kernel_release`). | +| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule + **manual** dispatch. Downloads `Image` from **`kernel-nightly`** by default (override via `kernel_release`). | + +Because GitHub Actions cannot natively “watch” another repository’s pushes, the +`v9fs/linux` repo should call `repository_dispatch` on `v9fs/test` when branches change +(see the header comment in `linux-kernel-publish.yml` for an example payload). + +### Kernel images as packages (GHCR) + +Published kernels are also pushed to GitHub Packages (GHCR) as an OCI artifact: + +- **Package**: `ghcr.io/v9fs/v9fs-test-kernel` +- **Tags**: + - `linux-` (immutable) + - The **release tag** you passed (e.g. `kernel-main`, `kernel-nightly`, `kernel-6.12.0`) + +```bash +oras pull ghcr.io/v9fs/v9fs-test-kernel:linux- -o . +tar -tzf kernel-image.tar.gz | head +``` + +### Kernel images as direct downloads (GitHub Releases) + +The publish workflow uploads the **arm64** kernel `Image` as a stable release asset: + +- **Rolling mainline**: `https://github.com/v9fs/test/releases/download/kernel-main/Image` +- **Rolling nightly**: `https://github.com/v9fs/test/releases/download/kernel-nightly/Image` +- **Versioned** (example): `https://github.com/v9fs/test/releases/download/kernel-6.12.0/Image` + + +Log artifact names (avoid collisions when jobs run in parallel): + +- CI manual/push: `test-results-ci`, `test-results-latency` +- Nightly: `test-results-regression`, `test-results-latency` + diff --git a/old/rework-take-1/TODO.md b/old/rework-take-1/TODO.md new file mode 100644 index 0000000..e4ad804 --- /dev/null +++ b/old/rework-take-1/TODO.md @@ -0,0 +1,4 @@ +## TODO + +- Decide whether to keep `ubuntu-latest` runners or switch back to self-hosted for KVM acceleration. +- Remove or clearly fence legacy SSH-based helpers (`test.bash`, `scripts/cpu`) if they are no longer part of the supported workflow. \ No newline at end of file diff --git a/fstabs-regress/virtio-mmap b/old/rework-take-1/fstabs-regress/virtio-mmap similarity index 100% rename from fstabs-regress/virtio-mmap rename to old/rework-take-1/fstabs-regress/virtio-mmap diff --git a/fstabs-regress/virtio-nocache b/old/rework-take-1/fstabs-regress/virtio-nocache similarity index 100% rename from fstabs-regress/virtio-nocache rename to old/rework-take-1/fstabs-regress/virtio-nocache diff --git a/fstabs-regress/virtio-ra b/old/rework-take-1/fstabs-regress/virtio-ra similarity index 100% rename from fstabs-regress/virtio-ra rename to old/rework-take-1/fstabs-regress/virtio-ra diff --git a/fstabs-short-old/qemu-loose b/old/rework-take-1/fstabs-short-old/qemu-loose similarity index 100% rename from fstabs-short-old/qemu-loose rename to old/rework-take-1/fstabs-short-old/qemu-loose diff --git a/fstabs-short-old/qemu-mmap b/old/rework-take-1/fstabs-short-old/qemu-mmap similarity index 100% rename from fstabs-short-old/qemu-mmap rename to old/rework-take-1/fstabs-short-old/qemu-mmap diff --git a/fstabs-short-old/virtio-fscache b/old/rework-take-1/fstabs-short-old/virtio-fscache similarity index 100% rename from fstabs-short-old/virtio-fscache rename to old/rework-take-1/fstabs-short-old/virtio-fscache diff --git a/fstabs-short-old/virtio-loose b/old/rework-take-1/fstabs-short-old/virtio-loose similarity index 100% rename from fstabs-short-old/virtio-loose rename to old/rework-take-1/fstabs-short-old/virtio-loose diff --git a/fstabs-short-old/virtio-mmap b/old/rework-take-1/fstabs-short-old/virtio-mmap similarity index 100% rename from fstabs-short-old/virtio-mmap rename to old/rework-take-1/fstabs-short-old/virtio-mmap diff --git a/fstabs-short-old/virtio-nocache b/old/rework-take-1/fstabs-short-old/virtio-nocache similarity index 100% rename from fstabs-short-old/virtio-nocache rename to old/rework-take-1/fstabs-short-old/virtio-nocache diff --git a/fstabs-short/qemu-loose b/old/rework-take-1/fstabs-short/qemu-loose similarity index 100% rename from fstabs-short/qemu-loose rename to old/rework-take-1/fstabs-short/qemu-loose diff --git a/fstabs-short/qemu-mmap b/old/rework-take-1/fstabs-short/qemu-mmap similarity index 100% rename from fstabs-short/qemu-mmap rename to old/rework-take-1/fstabs-short/qemu-mmap diff --git a/fstabs-short/virtio-fscache b/old/rework-take-1/fstabs-short/virtio-fscache similarity index 100% rename from fstabs-short/virtio-fscache rename to old/rework-take-1/fstabs-short/virtio-fscache diff --git a/fstabs-short/virtio-loose b/old/rework-take-1/fstabs-short/virtio-loose similarity index 100% rename from fstabs-short/virtio-loose rename to old/rework-take-1/fstabs-short/virtio-loose diff --git a/fstabs-short/virtio-mmap b/old/rework-take-1/fstabs-short/virtio-mmap similarity index 100% rename from fstabs-short/virtio-mmap rename to old/rework-take-1/fstabs-short/virtio-mmap diff --git a/fstabs-short/virtio-nocache b/old/rework-take-1/fstabs-short/virtio-nocache similarity index 100% rename from fstabs-short/virtio-nocache rename to old/rework-take-1/fstabs-short/virtio-nocache diff --git a/fstabs-short/virtio-ra b/old/rework-take-1/fstabs-short/virtio-ra similarity index 100% rename from fstabs-short/virtio-ra rename to old/rework-take-1/fstabs-short/virtio-ra diff --git a/fstabs-smoke/virtio-fscache b/old/rework-take-1/fstabs-smoke/virtio-fscache similarity index 100% rename from fstabs-smoke/virtio-fscache rename to old/rework-take-1/fstabs-smoke/virtio-fscache diff --git a/good/fscache/dbench.log b/old/rework-take-1/good/fscache/dbench.log similarity index 100% rename from good/fscache/dbench.log rename to old/rework-take-1/good/fscache/dbench.log diff --git a/good/fscache/dbench.time b/old/rework-take-1/good/fscache/dbench.time similarity index 100% rename from good/fscache/dbench.time rename to old/rework-take-1/good/fscache/dbench.time diff --git a/good/fscache/fsx-mmap.log b/old/rework-take-1/good/fscache/fsx-mmap.log similarity index 100% rename from good/fscache/fsx-mmap.log rename to old/rework-take-1/good/fscache/fsx-mmap.log diff --git a/good/fscache/fsx-mmap.time b/old/rework-take-1/good/fscache/fsx-mmap.time similarity index 100% rename from good/fscache/fsx-mmap.time rename to old/rework-take-1/good/fscache/fsx-mmap.time diff --git a/good/fscache/fsx.log b/old/rework-take-1/good/fscache/fsx.log similarity index 100% rename from good/fscache/fsx.log rename to old/rework-take-1/good/fscache/fsx.log diff --git a/good/fscache/fsx.time b/old/rework-take-1/good/fscache/fsx.time similarity index 100% rename from good/fscache/fsx.time rename to old/rework-take-1/good/fscache/fsx.time diff --git a/good/fscache/ldconfig.log b/old/rework-take-1/good/fscache/ldconfig.log similarity index 100% rename from good/fscache/ldconfig.log rename to old/rework-take-1/good/fscache/ldconfig.log diff --git a/good/fscache/ldconfig.time b/old/rework-take-1/good/fscache/ldconfig.time similarity index 100% rename from good/fscache/ldconfig.time rename to old/rework-take-1/good/fscache/ldconfig.time diff --git a/good/fscache/postmark.log b/old/rework-take-1/good/fscache/postmark.log similarity index 100% rename from good/fscache/postmark.log rename to old/rework-take-1/good/fscache/postmark.log diff --git a/good/fscache/postmark.time b/old/rework-take-1/good/fscache/postmark.time similarity index 100% rename from good/fscache/postmark.time rename to old/rework-take-1/good/fscache/postmark.time diff --git a/good/fscache/results.log b/old/rework-take-1/good/fscache/results.log similarity index 100% rename from good/fscache/results.log rename to old/rework-take-1/good/fscache/results.log diff --git a/good/loose/dbench.log b/old/rework-take-1/good/loose/dbench.log similarity index 100% rename from good/loose/dbench.log rename to old/rework-take-1/good/loose/dbench.log diff --git a/good/loose/dbench.time b/old/rework-take-1/good/loose/dbench.time similarity index 100% rename from good/loose/dbench.time rename to old/rework-take-1/good/loose/dbench.time diff --git a/good/loose/fsx-mmap.log b/old/rework-take-1/good/loose/fsx-mmap.log similarity index 100% rename from good/loose/fsx-mmap.log rename to old/rework-take-1/good/loose/fsx-mmap.log diff --git a/good/loose/fsx-mmap.time b/old/rework-take-1/good/loose/fsx-mmap.time similarity index 100% rename from good/loose/fsx-mmap.time rename to old/rework-take-1/good/loose/fsx-mmap.time diff --git a/good/loose/fsx.log b/old/rework-take-1/good/loose/fsx.log similarity index 100% rename from good/loose/fsx.log rename to old/rework-take-1/good/loose/fsx.log diff --git a/good/loose/fsx.time b/old/rework-take-1/good/loose/fsx.time similarity index 100% rename from good/loose/fsx.time rename to old/rework-take-1/good/loose/fsx.time diff --git a/good/loose/ldconfig.log b/old/rework-take-1/good/loose/ldconfig.log similarity index 100% rename from good/loose/ldconfig.log rename to old/rework-take-1/good/loose/ldconfig.log diff --git a/good/loose/ldconfig.time b/old/rework-take-1/good/loose/ldconfig.time similarity index 100% rename from good/loose/ldconfig.time rename to old/rework-take-1/good/loose/ldconfig.time diff --git a/good/loose/postmark.log b/old/rework-take-1/good/loose/postmark.log similarity index 100% rename from good/loose/postmark.log rename to old/rework-take-1/good/loose/postmark.log diff --git a/good/loose/postmark.time b/old/rework-take-1/good/loose/postmark.time similarity index 100% rename from good/loose/postmark.time rename to old/rework-take-1/good/loose/postmark.time diff --git a/good/loose/results.log b/old/rework-take-1/good/loose/results.log similarity index 100% rename from good/loose/results.log rename to old/rework-take-1/good/loose/results.log diff --git a/good/mmap/dbench.log b/old/rework-take-1/good/mmap/dbench.log similarity index 100% rename from good/mmap/dbench.log rename to old/rework-take-1/good/mmap/dbench.log diff --git a/good/mmap/dbench.time b/old/rework-take-1/good/mmap/dbench.time similarity index 100% rename from good/mmap/dbench.time rename to old/rework-take-1/good/mmap/dbench.time diff --git a/good/mmap/fsx-mmap.log b/old/rework-take-1/good/mmap/fsx-mmap.log similarity index 100% rename from good/mmap/fsx-mmap.log rename to old/rework-take-1/good/mmap/fsx-mmap.log diff --git a/good/mmap/fsx-mmap.time b/old/rework-take-1/good/mmap/fsx-mmap.time similarity index 100% rename from good/mmap/fsx-mmap.time rename to old/rework-take-1/good/mmap/fsx-mmap.time diff --git a/good/mmap/fsx.log b/old/rework-take-1/good/mmap/fsx.log similarity index 100% rename from good/mmap/fsx.log rename to old/rework-take-1/good/mmap/fsx.log diff --git a/good/mmap/fsx.time b/old/rework-take-1/good/mmap/fsx.time similarity index 100% rename from good/mmap/fsx.time rename to old/rework-take-1/good/mmap/fsx.time diff --git a/good/mmap/ldconfig.log b/old/rework-take-1/good/mmap/ldconfig.log similarity index 100% rename from good/mmap/ldconfig.log rename to old/rework-take-1/good/mmap/ldconfig.log diff --git a/good/mmap/ldconfig.time b/old/rework-take-1/good/mmap/ldconfig.time similarity index 100% rename from good/mmap/ldconfig.time rename to old/rework-take-1/good/mmap/ldconfig.time diff --git a/good/mmap/postmark.log b/old/rework-take-1/good/mmap/postmark.log similarity index 100% rename from good/mmap/postmark.log rename to old/rework-take-1/good/mmap/postmark.log diff --git a/good/mmap/postmark.time b/old/rework-take-1/good/mmap/postmark.time similarity index 100% rename from good/mmap/postmark.time rename to old/rework-take-1/good/mmap/postmark.time diff --git a/good/mmap/results.log b/old/rework-take-1/good/mmap/results.log similarity index 100% rename from good/mmap/results.log rename to old/rework-take-1/good/mmap/results.log diff --git a/good/nocache/dbench.log b/old/rework-take-1/good/nocache/dbench.log similarity index 100% rename from good/nocache/dbench.log rename to old/rework-take-1/good/nocache/dbench.log diff --git a/good/nocache/dbench.time b/old/rework-take-1/good/nocache/dbench.time similarity index 100% rename from good/nocache/dbench.time rename to old/rework-take-1/good/nocache/dbench.time diff --git a/good/nocache/fsx-mmap.log b/old/rework-take-1/good/nocache/fsx-mmap.log similarity index 100% rename from good/nocache/fsx-mmap.log rename to old/rework-take-1/good/nocache/fsx-mmap.log diff --git a/good/nocache/fsx-mmap.time b/old/rework-take-1/good/nocache/fsx-mmap.time similarity index 100% rename from good/nocache/fsx-mmap.time rename to old/rework-take-1/good/nocache/fsx-mmap.time diff --git a/good/nocache/fsx.log b/old/rework-take-1/good/nocache/fsx.log similarity index 100% rename from good/nocache/fsx.log rename to old/rework-take-1/good/nocache/fsx.log diff --git a/good/nocache/fsx.time b/old/rework-take-1/good/nocache/fsx.time similarity index 100% rename from good/nocache/fsx.time rename to old/rework-take-1/good/nocache/fsx.time diff --git a/good/nocache/ldconfig.log b/old/rework-take-1/good/nocache/ldconfig.log similarity index 100% rename from good/nocache/ldconfig.log rename to old/rework-take-1/good/nocache/ldconfig.log diff --git a/good/nocache/ldconfig.time b/old/rework-take-1/good/nocache/ldconfig.time similarity index 100% rename from good/nocache/ldconfig.time rename to old/rework-take-1/good/nocache/ldconfig.time diff --git a/good/nocache/postmark.log b/old/rework-take-1/good/nocache/postmark.log similarity index 100% rename from good/nocache/postmark.log rename to old/rework-take-1/good/nocache/postmark.log diff --git a/good/nocache/postmark.time b/old/rework-take-1/good/nocache/postmark.time similarity index 100% rename from good/nocache/postmark.time rename to old/rework-take-1/good/nocache/postmark.time diff --git a/good/nocache/results.log b/old/rework-take-1/good/nocache/results.log similarity index 100% rename from good/nocache/results.log rename to old/rework-take-1/good/nocache/results.log diff --git a/good/ra/dbench.log b/old/rework-take-1/good/ra/dbench.log similarity index 100% rename from good/ra/dbench.log rename to old/rework-take-1/good/ra/dbench.log diff --git a/good/ra/dbench.time b/old/rework-take-1/good/ra/dbench.time similarity index 100% rename from good/ra/dbench.time rename to old/rework-take-1/good/ra/dbench.time diff --git a/good/ra/fsx-mmap.log b/old/rework-take-1/good/ra/fsx-mmap.log similarity index 100% rename from good/ra/fsx-mmap.log rename to old/rework-take-1/good/ra/fsx-mmap.log diff --git a/good/ra/fsx-mmap.time b/old/rework-take-1/good/ra/fsx-mmap.time similarity index 100% rename from good/ra/fsx-mmap.time rename to old/rework-take-1/good/ra/fsx-mmap.time diff --git a/good/ra/fsx.log b/old/rework-take-1/good/ra/fsx.log similarity index 100% rename from good/ra/fsx.log rename to old/rework-take-1/good/ra/fsx.log diff --git a/good/ra/fsx.time b/old/rework-take-1/good/ra/fsx.time similarity index 100% rename from good/ra/fsx.time rename to old/rework-take-1/good/ra/fsx.time diff --git a/good/ra/ldconfig.log b/old/rework-take-1/good/ra/ldconfig.log similarity index 100% rename from good/ra/ldconfig.log rename to old/rework-take-1/good/ra/ldconfig.log diff --git a/good/ra/ldconfig.time b/old/rework-take-1/good/ra/ldconfig.time similarity index 100% rename from good/ra/ldconfig.time rename to old/rework-take-1/good/ra/ldconfig.time diff --git a/good/ra/postmark.log b/old/rework-take-1/good/ra/postmark.log similarity index 100% rename from good/ra/postmark.log rename to old/rework-take-1/good/ra/postmark.log diff --git a/good/ra/postmark.time b/old/rework-take-1/good/ra/postmark.time similarity index 100% rename from good/ra/postmark.time rename to old/rework-take-1/good/ra/postmark.time diff --git a/good/ra/results.log b/old/rework-take-1/good/ra/results.log similarity index 100% rename from good/ra/results.log rename to old/rework-take-1/good/ra/results.log diff --git a/parser/dbench.py b/old/rework-take-1/parser/dbench.py similarity index 100% rename from parser/dbench.py rename to old/rework-take-1/parser/dbench.py diff --git a/qemu-new.bash b/old/rework-take-1/qemu-new.bash similarity index 100% rename from qemu-new.bash rename to old/rework-take-1/qemu-new.bash diff --git a/qemu-old.bash b/old/rework-take-1/qemu-old.bash similarity index 100% rename from qemu-old.bash rename to old/rework-take-1/qemu-old.bash diff --git a/qemu.bash b/old/rework-take-1/qemu.bash similarity index 100% rename from qemu.bash rename to old/rework-take-1/qemu.bash diff --git a/scripts/cpu b/old/rework-take-1/scripts/cpu similarity index 100% rename from scripts/cpu rename to old/rework-take-1/scripts/cpu diff --git a/scripts/v9fs-build-initrd b/old/rework-take-1/scripts/v9fs-build-initrd similarity index 100% rename from scripts/v9fs-build-initrd rename to old/rework-take-1/scripts/v9fs-build-initrd diff --git a/scripts/v9fs-build-kernel b/old/rework-take-1/scripts/v9fs-build-kernel similarity index 100% rename from scripts/v9fs-build-kernel rename to old/rework-take-1/scripts/v9fs-build-kernel diff --git a/scripts/v9fs-entrypoint b/old/rework-take-1/scripts/v9fs-entrypoint similarity index 100% rename from scripts/v9fs-entrypoint rename to old/rework-take-1/scripts/v9fs-entrypoint diff --git a/scripts/v9fs-export-kernel b/old/rework-take-1/scripts/v9fs-export-kernel similarity index 100% rename from scripts/v9fs-export-kernel rename to old/rework-take-1/scripts/v9fs-export-kernel diff --git a/scripts/v9fs-guest-run b/old/rework-take-1/scripts/v9fs-guest-run similarity index 100% rename from scripts/v9fs-guest-run rename to old/rework-take-1/scripts/v9fs-guest-run diff --git a/scripts/v9fs-run-tests b/old/rework-take-1/scripts/v9fs-run-tests similarity index 100% rename from scripts/v9fs-run-tests rename to old/rework-take-1/scripts/v9fs-run-tests diff --git a/test.bash b/old/rework-take-1/test.bash similarity index 100% rename from test.bash rename to old/rework-take-1/test.bash diff --git a/tests/ci/dbench.bash b/old/rework-take-1/tests/ci/dbench.bash similarity index 100% rename from tests/ci/dbench.bash rename to old/rework-take-1/tests/ci/dbench.bash diff --git a/tests/ci/fsx-mmap.bash b/old/rework-take-1/tests/ci/fsx-mmap.bash similarity index 100% rename from tests/ci/fsx-mmap.bash rename to old/rework-take-1/tests/ci/fsx-mmap.bash diff --git a/tests/ci/fsx.bash b/old/rework-take-1/tests/ci/fsx.bash similarity index 100% rename from tests/ci/fsx.bash rename to old/rework-take-1/tests/ci/fsx.bash diff --git a/tests/ci/ldconfig.bash b/old/rework-take-1/tests/ci/ldconfig.bash similarity index 100% rename from tests/ci/ldconfig.bash rename to old/rework-take-1/tests/ci/ldconfig.bash diff --git a/tests/ci/postmark.bash b/old/rework-take-1/tests/ci/postmark.bash similarity index 100% rename from tests/ci/postmark.bash rename to old/rework-take-1/tests/ci/postmark.bash diff --git a/tests/latency/dbench.bash b/old/rework-take-1/tests/latency/dbench.bash similarity index 100% rename from tests/latency/dbench.bash rename to old/rework-take-1/tests/latency/dbench.bash From e66dbc438bc1d2b1695ef42050a42d16bdbe787f Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:36:13 -0500 Subject: [PATCH 21/69] Rebuild harness: guest-direct 9p smoke test + CI - Minimal Docker+QEMU harness that exports /workspaces/share/tmpdir via virtio-9p\n- Initrd runs guest smoke test on the 9p mount and writes guest.log + guest.exitcode\n- CI workflow downloads arm64 Image release asset and runs the smoke test\n- Kernel publish workflow builds arm64 Image and uploads to a release tag Made-with: Cursor --- .github/workflows/ci.yml | 77 ++++++++++++++++++++++ .github/workflows/linux-kernel-publish.yml | 68 +++++++++++++++++++ CHANGES.md | 4 ++ Dockerfile | 35 ++++++++++ Makefile | 20 ++++++ README.md | 27 ++++++++ TODO.md | 4 +- qemu.bash | 44 +++++++++++++ scripts/v9fs-build-initrd | 68 +++++++++++++++++++ scripts/v9fs-entrypoint | 15 +++++ scripts/v9fs-guest-run | 56 ++++++++++++++++ scripts/v9fs-run-tests | 59 +++++++++++++++++ 12 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/linux-kernel-publish.yml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 qemu.bash create mode 100644 scripts/v9fs-build-initrd create mode 100644 scripts/v9fs-entrypoint create mode 100644 scripts/v9fs-guest-run create mode 100644 scripts/v9fs-run-tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8d0d69f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: Harness CI + +on: + push: + workflow_dispatch: + inputs: + kernel_release: + description: 'Release tag that provides arm64 Image (default: kernel-main)' + required: true + default: 'kernel-main' + type: string + +permissions: + contents: read + +jobs: + smoke: + runs-on: ubuntu-24.04-arm + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-main' }} + steps: + - name: Checkout harness + uses: actions/checkout@v4 + + - name: Download published kernel Image + run: | + set -euo pipefail + mkdir -p kernel/.build/arch/arm64/boot + gh release download "${KERNEL_RELEASE}" -p Image -D kernel/.build/arch/arm64/boot --clobber + ls -la kernel/.build/arch/arm64/boot/Image + + - name: Build Docker environment + run: docker build -t v9fs-test-env:local . + + - name: Run smoke test (guest-direct) + run: | + mkdir -p "$GITHUB_WORKSPACE/tmp" + docker run --rm --privileged \ + -e KERNELBUILD=/workspaces/kernel/.build \ + -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ + -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ + -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc "v9fs-run-tests smoke" + + - name: Dump logs on failure + if: failure() + run: | + set -euo pipefail + echo "==== logs tree ====" + (ls -R logs || true) + echo "==== qemu.log (tail) ====" + for f in logs/*/qemu.log; do + echo "---- $f ----" + tail -n 250 "$f" || true + done + echo "==== guest.log (tail) ====" + for f in logs/*/guest.log; do + echo "---- $f ----" + tail -n 250 "$f" || true + done + + - name: Fix log permissions (for artifact upload) + if: always() + run: | + sudo chmod -R a+rX logs || true + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: smoke-logs + path: logs + retention-days: 7 + diff --git a/.github/workflows/linux-kernel-publish.yml b/.github/workflows/linux-kernel-publish.yml new file mode 100644 index 0000000..c2206d3 --- /dev/null +++ b/.github/workflows/linux-kernel-publish.yml @@ -0,0 +1,68 @@ +name: Publish Linux kernel (arm64 Image) + +on: + repository_dispatch: + types: [publish-kernel-image] + workflow_dispatch: + inputs: + linux_repository: + description: 'Kernel GitHub repository (owner/name)' + required: true + default: 'v9fs/linux' + type: string + linux_ref: + description: 'Git ref to build (branch, tag, or SHA)' + required: true + default: 'main' + type: string + release_tag: + description: 'Release tag to publish/update (kernel-main, kernel-nightly, kernel-)' + required: true + default: 'kernel-main' + type: string + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-24.04-arm + steps: + - name: Checkout harness (Dockerfile + scripts) + uses: actions/checkout@v4 + + - name: Checkout kernel + uses: actions/checkout@v4 + with: + repository: ${{ inputs.linux_repository || github.event.client_payload.linux_repository || 'v9fs/linux' }} + ref: ${{ inputs.linux_ref || github.event.client_payload.linux_ref || 'main' }} + path: linux + + - name: Build Docker environment + run: docker build -t v9fs-test-env:local . + + - name: Build kernel (arm64 Image) + run: | + mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" + docker run --rm --privileged \ + -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ + -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ + -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ + -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + v9fs-test-env:local \ + bash -lc 'set -euo pipefail; mkdir -p /workspaces/linux/.build; if [ ! -f /workspaces/linux/.build/.config ]; then make -C /workspaces/linux O=/workspaces/linux/.build defconfig; fi; make -C /workspaces/linux O=/workspaces/linux/.build -j\"$(nproc)\"; mkdir -p /workspaces/kernel/.build/arch/arm64/boot; cp -f /workspaces/linux/.build/arch/arm64/boot/Image /workspaces/kernel/.build/arch/arm64/boot/Image' + + - name: Publish Image as release asset + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.release_tag || github.event.client_payload.release_tag || 'kernel-main' }} + run: | + set -euo pipefail + img="kernel/.build/arch/arm64/boot/Image" + test -f "$img" + gh release view "$TAG" >/dev/null 2>&1 || \ + gh release create "$TAG" --title "$TAG" --notes "Automated arm64 kernel Image." --prerelease + cp -f "$img" Image + gh release upload "$TAG" Image --clobber + diff --git a/CHANGES.md b/CHANGES.md index 82688c6..9b782b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,8 @@ ## Unreleased - Clean-slate rework started; prior implementation preserved under `old/rework-take-1/` (tag `rework-take-1`). +- Rebuild harness (take 2): minimal Docker+QEMU guest-direct smoke test that mounts a 9p export and validates basic filesystem operations. +- Add GitHub Actions: + - `linux-kernel-publish.yml` to build/publish arm64 `Image` as a release asset + - `ci.yml` to download `Image` and run the smoke harness in CI diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9410d9e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + cpio \ + curl \ + file \ + git \ + make \ + gcc \ + libc6-dev \ + python3 \ + qemu-system-arm \ + qemu-utils \ + rsync \ + time \ + util-linux \ + busybox-static \ + && rm -rf /var/lib/apt/lists/* + +# Stable user/home matching harness assumptions. +RUN useradd -m -s /bin/bash v9fs-test \ + && mkdir -p /workspaces /workspaces/tmp \ + && chown -R v9fs-test:v9fs-test /home/v9fs-test /workspaces + +COPY scripts/ /usr/local/bin/ +RUN chmod +x /usr/local/bin/* + +WORKDIR /home/v9fs-test/test + +ENTRYPOINT ["/usr/local/bin/v9fs-entrypoint"] + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ed17432 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY: docker-build docker-smoke + +IMAGE ?= v9fs-test-env:local +KERNEL_RELEASE ?= kernel-main + +docker-build: + docker build -t $(IMAGE) . + +docker-smoke: docker-build + mkdir -p ./tmp ./kernel + @echo "Place kernel Image at ./kernel/.build/arch/arm64/boot/Image (or download from release $(KERNEL_RELEASE))" + docker run --rm --privileged \ + -e KERNELBUILD=/workspaces/kernel/.build \ + -v "$$(pwd):/home/v9fs-test/test" \ + -v "$$(pwd)/kernel:/workspaces/kernel" \ + -v "$$(pwd)/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + $(IMAGE) \ + bash -lc "v9fs-run-tests smoke" + diff --git a/README.md b/README.md index f3e51da..3d5e616 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,33 @@ The previous implementation snapshot is preserved at: - Git tag: `rework-take-1` - Directory: `old/rework-take-1/` +## Current rebuild (take 2) + +This repo now contains a **minimal 9p client smoke test harness**: + +- QEMU runs an **arm64** Linux kernel `Image` +- QEMU exports a host directory via **virtio-9p** +- An initrd runs the smoke test **inside the guest** (no SSH) +- The test exercises basic filesystem operations on the 9p mount + +### Local smoke run (Docker + QEMU) + +1) Build the image: + +```bash +make docker-build +``` + +2) Put a kernel `Image` at `./kernel/.build/arch/arm64/boot/Image` + +3) Run the smoke test: + +```bash +make docker-smoke +``` + +Logs land under `./logs//` (QEMU serial is `qemu.log`; guest writes `guest.log` + `guest.exitcode`). + # v9fs test harness This repository contains **test code and scripts** for exercising the Linux **9p (v9fs)** filesystem. diff --git a/TODO.md b/TODO.md index d673d5c..ab0df21 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,8 @@ ## TODO -- Rebuild the harness from scratch (see `old/rework-take-1/` for reference). +- Extend smoke test into a small suite (create/rename/unlink, fsync, directory traversal, large file IO). +- Decide whether to adopt a u-root/u-root+cpu initramfs for richer tooling distribution. +- Wire `v9fs/linux` to trigger `repository_dispatch` into `linux-kernel-publish.yml`. ## TODO diff --git a/qemu.bash b/qemu.bash new file mode 100644 index 0000000..da37855 --- /dev/null +++ b/qemu.bash @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARCH="${ARCH:-$(uname -m)}" +KERNELBUILD="${KERNELBUILD:-/workspaces/kernel/.build}" +INITRD="${INITRD:-/home/v9fs-test/initrd.cpio}" +LOG="${QEMULOG:-/home/v9fs-test/qemu.log}" +PIDFILE="${PIDFILE:-/home/v9fs-test/qemu.pid}" +FSDEV_PATH="${FSDEV_PATH:-/workspaces/share}" + +if test -f "${PIDFILE}"; then + kill "$(cat "${PIDFILE}")" 2>/dev/null || true +fi + +if [ "${ARCH}" = "aarch64" ]; then + QEMU="qemu-system-aarch64" + KERNEL="${KERNELBUILD}/arch/arm64/boot/Image" + MACHINE="virt" + APPEND="earlycon console=ttyAMA0" + QEMUCPU="${QEMUCPU:-cortex-a57}" + EXTRA="" +else + echo "Unsupported ARCH=${ARCH} (expected aarch64)" + exit 2 +fi + +exec ${QEMU} \ + -kernel "${KERNEL}" \ + -cpu "${QEMUCPU}" \ + -machine "${MACHINE}" \ + -smp 4 \ + -m 4096m \ + -initrd "${INITRD}" \ + -object rng-random,filename=/dev/urandom,id=rng0 \ + -device virtio-rng-pci,rng=rng0 \ + -device virtio-net-pci,netdev=n1 \ + -netdev user,id=n1 \ + -serial "file:${LOG}" \ + -fsdev "local,security_model=none,writeout=immediate,id=fsdev0,path=${FSDEV_PATH}" \ + -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare \ + -append "${APPEND} ${EXTRA_APPEND:-}" \ + ${EXTRA} \ + -daemonize -display none -pidfile "${PIDFILE}" + diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd new file mode 100644 index 0000000..cf863d0 --- /dev/null +++ b/scripts/v9fs-build-initrd @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +out="${1:-/home/v9fs-test/initrd.cpio}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +mkdir -p "$work"/{bin,sbin,etc,proc,sys,dev,run,tmp,mnt/9} + +cat >"$work/init" <<'EOF' +#!/bin/sh +set -eu + +export PATH=/bin:/sbin + +mount -t proc proc /proc +mount -t sysfs sysfs /sys +mount -t devtmpfs devtmpfs /dev || true + +mkdir -p /run /tmp /mnt/9 + +# Mount hostshare (virtio-9p) exported by QEMU. +if ! mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then + echo "initrd: ERROR: failed to mount hostshare on /mnt/9" +fi + +cmdline="$(cat /proc/cmdline 2>/dev/null || true)" +case "$cmdline" in + *v9fs.run=1*) + tests="$(echo "$cmdline" | sed -n 's/.*v9fs.tests=\\([^ ]*\\).*/\\1/p')" + ts="$(echo "$cmdline" | sed -n 's/.*v9fs.ts=\\([^ ]*\\).*/\\1/p')" + : "${tests:=smoke}" + if [ -n "${ts:-}" ]; then + export TIMESTAMP="$ts" + fi + + echo "initrd: running guest tests tests=$tests ts=${TIMESTAMP:-unset}" + if [ -x /mnt/9/test/scripts/v9fs-guest-run ]; then + /bin/sh /mnt/9/test/scripts/v9fs-guest-run "$tests" || true + else + echo "initrd: missing /mnt/9/test/scripts/v9fs-guest-run" + ls -la /mnt/9/test/scripts 2>/dev/null || true + fi + sync 2>/dev/null || true + poweroff -f || halt -f || reboot -f + ;; +esac + +echo "initrd up; interactive shell" +exec sh +EOF +chmod +x "$work/init" + +cat >"$work/etc/passwd" <<'EOF' +root:x:0:0:root:/root:/bin/sh +EOF +cat >"$work/etc/group" <<'EOF' +root:x:0: +EOF + +cp /bin/busybox "$work/bin/busybox" +for a in sh mount mkdir ln ls cat echo sleep uname ps kill date sed rm sync poweroff halt reboot; do + ln -s /bin/busybox "$work/bin/$a" 2>/dev/null || true +done + +(cd "$work" && find . -print0 | cpio --null -ov --format=newc) >"$out" +echo "Wrote initrd to $out" + diff --git a/scripts/v9fs-entrypoint b/scripts/v9fs-entrypoint new file mode 100644 index 0000000..0070268 --- /dev/null +++ b/scripts/v9fs-entrypoint @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +umask 022 + +mkdir -p /workspaces/tmp +mkdir -p /home/v9fs-test + +# Default action: run the harness. +if [ "${1:-}" = "" ]; then + exec /usr/local/bin/v9fs-run-tests +fi + +exec "$@" + diff --git a/scripts/v9fs-guest-run b/scripts/v9fs-guest-run new file mode 100644 index 0000000..3ddbece --- /dev/null +++ b/scripts/v9fs-guest-run @@ -0,0 +1,56 @@ +#!/bin/sh +set -eu + +tests="${1:-smoke}" + +repo="/mnt/9/test" +ts="${TIMESTAMP:-$(date +%s)}" + +logdir="$repo/logs/$ts" +mkdir -p "$logdir" +rm -f "$repo/logs/current" +ln -s "$ts" "$repo/logs/current" + +guestlog="$logdir/guest.log" +rcfile="$logdir/guest.exitcode" + +exec >"$guestlog" 2>&1 + +echo "GUEST: tests=$tests ts=$ts" +echo "GUEST: mounted /mnt/9: $(mount | grep ' /mnt/9 ' || true)" + +tmpdir="/mnt/9/tmpdir" +mkdir -p "$tmpdir" + +rc=0 + +case "$tests" in + smoke|*) + t="$tmpdir/smoke.$$" + mkdir -p "$t" + + echo "GUEST: write/read basic file" + printf 'hello-9p\n' >"$t/hello.txt" || rc=1 + [ "$(cat "$t/hello.txt" 2>/dev/null || true)" = "hello-9p" ] || rc=1 + + echo "GUEST: rename + append" + mv "$t/hello.txt" "$t/renamed.txt" || rc=1 + printf 'more\n' >>"$t/renamed.txt" || rc=1 + grep -q 'more' "$t/renamed.txt" || rc=1 + + echo "GUEST: mkdir/list/rmdir" + mkdir -p "$t/dirA" || rc=1 + ls -la "$t" >/dev/null 2>&1 || rc=1 + rmdir "$t/dirA" || rc=1 + + echo "GUEST: unlink cleanup" + rm -f "$t/renamed.txt" || rc=1 + rmdir "$t" || rc=1 + ;; +esac + +echo "$rc" >"$rcfile" 2>/dev/null || true +sync 2>/dev/null || true +echo "GUEST: done rc=$rc" +exit "$rc" + diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests new file mode 100644 index 0000000..1b65bc5 --- /dev/null +++ b/scripts/v9fs-run-tests @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +tests="${1:-smoke}" +export TIMESTAMP="${TIMESTAMP:-$(date +%s)}" + +mkdir -p "logs/${TIMESTAMP}" +export QEMULOG="logs/${TIMESTAMP}/qemu.log" +export PIDFILE="/home/v9fs-test/qemu.pid" + +# Make a stable export root for 9p and ensure we have a temp directory to export. +mkdir -p /workspaces/share +mkdir -p /workspaces/share/test /workspaces/share/tmpdir /workspaces/share/kernel /workspaces/share/tmp + +mountpoint -q /workspaces/share/test || mount --bind /home/v9fs-test/test /workspaces/share/test +mountpoint -q /workspaces/share/kernel || mount --bind /workspaces/kernel /workspaces/share/kernel +mountpoint -q /workspaces/share/tmp || mount --bind /workspaces/tmp /workspaces/share/tmp + +export FSDEV_PATH="/workspaces/share" +export KERNELBUILD="${KERNELBUILD:-/workspaces/kernel/.build}" + +# Build initrd each run (cheap; keeps guest runner in sync). +/usr/local/bin/v9fs-build-initrd /home/v9fs-test/initrd.cpio + +# Ask initrd to run guest tests, and propagate timestamp so the guest writes +# guest.exitcode into the same logs// directory the host checks. +export EXTRA_APPEND="v9fs.run=1 v9fs.tests=${tests} v9fs.ts=${TIMESTAMP}" + +/home/v9fs-test/test/qemu.bash + +# Wait for QEMU to exit (guest powers off). +if [ -f "${PIDFILE}" ]; then + pid="$(cat "${PIDFILE}")" + while kill -0 "${pid}" 2>/dev/null; do + sleep 1 + done +fi + +chmod -R a+rX "logs/${TIMESTAMP}" 2>/dev/null || true + +guest_rc_file="logs/${TIMESTAMP}/guest.exitcode" +for _ in 1 2 3 4 5; do + [ -f "${guest_rc_file}" ] && break + sleep 1 +done + +if [ ! -f "${guest_rc_file}" ]; then + echo "WARNING: missing ${guest_rc_file}; treating as failure" + exit 2 +fi + +guest_rc="$(cat "${guest_rc_file}" 2>/dev/null || echo 1)" +if [ "${guest_rc}" != "0" ]; then + echo "Guest reported failures (rc=${guest_rc})" + exit "${guest_rc}" +fi + +echo "OK: guest rc=0 (logs at logs/${TIMESTAMP}/)" + From b62937b6b8e62d4e58699fcd89053aa6204169bd Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:36:53 -0500 Subject: [PATCH 22/69] Kernel publish: enable 9p client config Force-enable NET_9P/9P_FS + virtio transport options via scripts/config so published arm64 Image can mount virtio-9p in the smoke harness. Made-with: Cursor --- .github/workflows/linux-kernel-publish.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linux-kernel-publish.yml b/.github/workflows/linux-kernel-publish.yml index c2206d3..ef48817 100644 --- a/.github/workflows/linux-kernel-publish.yml +++ b/.github/workflows/linux-kernel-publish.yml @@ -51,7 +51,26 @@ jobs: -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ v9fs-test-env:local \ - bash -lc 'set -euo pipefail; mkdir -p /workspaces/linux/.build; if [ ! -f /workspaces/linux/.build/.config ]; then make -C /workspaces/linux O=/workspaces/linux/.build defconfig; fi; make -C /workspaces/linux O=/workspaces/linux/.build -j\"$(nproc)\"; mkdir -p /workspaces/kernel/.build/arch/arm64/boot; cp -f /workspaces/linux/.build/arch/arm64/boot/Image /workspaces/kernel/.build/arch/arm64/boot/Image' + bash -lc '\ + set -euo pipefail; \ + out=/workspaces/linux/.build; \ + mkdir -p \"$out\"; \ + if [ ! -f \"$out/.config\" ]; then \ + make -C /workspaces/linux O=\"$out\" defconfig; \ + fi; \ + cfg=/workspaces/linux/scripts/config; \ + if [ -x \"$cfg\" ]; then \ + \"$cfg\" --file \"$out/.config\" \ + -e NET_9P -e NET_9P_VIRTIO -e 9P_FS -e 9P_FS_POSIX_ACL \ + -e VIRTIO_PCI -e PCI; \ + make -C /workspaces/linux O=\"$out\" olddefconfig; \ + else \ + echo \"WARNING: missing scripts/config; relying on defconfig\"; \ + fi; \ + make -C /workspaces/linux O=\"$out\" -j\"$(nproc)\"; \ + mkdir -p /workspaces/kernel/.build/arch/arm64/boot; \ + cp -f \"$out/arch/arm64/boot/Image\" /workspaces/kernel/.build/arch/arm64/boot/Image; \ + ' - name: Publish Image as release asset env: From b585d4ac035269c64af4212d271321178d56ab5a Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:38:36 -0500 Subject: [PATCH 23/69] Use v9fs/docker image instead of custom Dockerfile - Remove custom Dockerfile/entrypoint\n- Run harness + kernel builds inside ghcr.io/v9fs/docker:latest\n- Invoke repo scripts from bind mount (./scripts/...)\n- Update CI workflows and Makefile/README accordingly Made-with: Cursor --- .github/workflows/ci.yml | 8 ++-- .github/workflows/linux-kernel-publish.yml | 9 ++-- AGENTS.md | 53 ++++++++++++++++++++++ Dockerfile | 35 -------------- Makefile | 11 ++--- README.md | 2 +- scripts/v9fs-entrypoint | 15 ------ scripts/v9fs-run-tests | 2 +- 8 files changed, 66 insertions(+), 69 deletions(-) create mode 100644 AGENTS.md delete mode 100644 Dockerfile delete mode 100644 scripts/v9fs-entrypoint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d0d69f..0aefa29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-main' }} + V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:latest steps: - name: Checkout harness uses: actions/checkout@v4 @@ -30,9 +31,6 @@ jobs: gh release download "${KERNEL_RELEASE}" -p Image -D kernel/.build/arch/arm64/boot --clobber ls -la kernel/.build/arch/arm64/boot/Image - - name: Build Docker environment - run: docker build -t v9fs-test-env:local . - - name: Run smoke test (guest-direct) run: | mkdir -p "$GITHUB_WORKSPACE/tmp" @@ -42,8 +40,8 @@ jobs: -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ - v9fs-test-env:local \ - bash -lc "v9fs-run-tests smoke" + "${V9FS_DOCKER_IMAGE}" \ + bash -lc "./scripts/v9fs-run-tests smoke" - name: Dump logs on failure if: failure() diff --git a/.github/workflows/linux-kernel-publish.yml b/.github/workflows/linux-kernel-publish.yml index ef48817..d2231c0 100644 --- a/.github/workflows/linux-kernel-publish.yml +++ b/.github/workflows/linux-kernel-publish.yml @@ -27,8 +27,10 @@ permissions: jobs: publish: runs-on: ubuntu-24.04-arm + env: + V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:latest steps: - - name: Checkout harness (Dockerfile + scripts) + - name: Checkout harness (scripts) uses: actions/checkout@v4 - name: Checkout kernel @@ -38,9 +40,6 @@ jobs: ref: ${{ inputs.linux_ref || github.event.client_payload.linux_ref || 'main' }} path: linux - - name: Build Docker environment - run: docker build -t v9fs-test-env:local . - - name: Build kernel (arm64 Image) run: | mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" @@ -50,7 +49,7 @@ jobs: -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ - v9fs-test-env:local \ + "${V9FS_DOCKER_IMAGE}" \ bash -lc '\ set -euo pipefail; \ out=/workspaces/linux/.build; \ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..63695d0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,53 @@ +# AGENTS.md + +This file captures the working preferences for AI agents contributing to this repo. +Tweak freely. + +## Branching and change hygiene + +- Do active work on `rework` unless told otherwise. +- Keep `README.md`, `CHANGES.md`, and `TODO.md` updated as changes land. +- Do not commit generated outputs (`logs/`, `kernel/`, `tmp/`, `initrd.cpio`, pid files). + +## Test philosophy +- Prefer **guest-direct execution** (Option A): run tests **inside the QEMU guest**. +- Avoid SSH/port-forwarding flows unless explicitly requested. +- use u-root based minimal initrd as root filesystem +- use u-root/cpu with NFS option to expose tools, benchmarks, tests, and results directories to the guest running in qemu +- be able to run as github actions or using act locally +- provide easy mechanism for running local tests (make or script based) +- verify workflow locally before pushing to github +- CI should surface failures (red dashboards) while still running the full suite: + - Run the whole matrix + - Record failures + - Exit non-zero at the end + +## Local/dev environment assumptions + +- Primary local dev is **macOS via Docker + QEMU**. +- Prefer solutions that work on Docker Desktop (no reliance on KVM). + +## CI architecture preferences +- Use http://github.com/v9fs/docker published base image instead of building custom docker for kernel build and/or test frameworks +- Default to **ARM64** (`ubuntu-24.04-arm`) for builds/tests unless asked otherwise. +- Build and/or test will be triggered by external triggers (such as v9fs/linux changes) or user request in addition to any changes to this repo +- Separate concerns: + - **Kernel publishing** workflow: builds `v9fs/linux` arm64 `Image` and publishes it. + - **Harness CI** workflows: download a published kernel `Image` and run tests. +- Publishing: + - Prefer a stable, `wget`-able GitHub Release asset `Image` tagged `kernel-main`, `kernel-nightly`, or `kernel-`. + - GHCR is optional/secondary; keep it consistent if used. + +## Logging and debuggability + +- Always preserve logs for failures (artifact upload `if: always()`). +- When tests fail, also dump the relevant tails into the CI console output: + - `logs/*/qemu.log` + - per-test `*.log` + - `guest.exitcode` markers (or equivalent) + +## Style + +- Prefer small, explicit scripts over complex magic. +- Keep paths stable and explicit (`/workspaces/share`, `kernel/.build/...`). +- Avoid large refactors unless requested; preserve working behavior first. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 9410d9e..0000000 --- a/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM ubuntu:24.04 - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y --no-install-recommends \ - bash \ - ca-certificates \ - cpio \ - curl \ - file \ - git \ - make \ - gcc \ - libc6-dev \ - python3 \ - qemu-system-arm \ - qemu-utils \ - rsync \ - time \ - util-linux \ - busybox-static \ - && rm -rf /var/lib/apt/lists/* - -# Stable user/home matching harness assumptions. -RUN useradd -m -s /bin/bash v9fs-test \ - && mkdir -p /workspaces /workspaces/tmp \ - && chown -R v9fs-test:v9fs-test /home/v9fs-test /workspaces - -COPY scripts/ /usr/local/bin/ -RUN chmod +x /usr/local/bin/* - -WORKDIR /home/v9fs-test/test - -ENTRYPOINT ["/usr/local/bin/v9fs-entrypoint"] - diff --git a/Makefile b/Makefile index ed17432..c509631 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,9 @@ -.PHONY: docker-build docker-smoke +.PHONY: docker-smoke -IMAGE ?= v9fs-test-env:local +IMAGE ?= ghcr.io/v9fs/docker:latest KERNEL_RELEASE ?= kernel-main -docker-build: - docker build -t $(IMAGE) . - -docker-smoke: docker-build +docker-smoke: mkdir -p ./tmp ./kernel @echo "Place kernel Image at ./kernel/.build/arch/arm64/boot/Image (or download from release $(KERNEL_RELEASE))" docker run --rm --privileged \ @@ -16,5 +13,5 @@ docker-smoke: docker-build -v "$$(pwd)/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ $(IMAGE) \ - bash -lc "v9fs-run-tests smoke" + bash -lc "./scripts/v9fs-run-tests smoke" diff --git a/README.md b/README.md index 3d5e616..e6c5438 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This repo now contains a **minimal 9p client smoke test harness**: 1) Build the image: ```bash -make docker-build +docker pull ghcr.io/v9fs/docker:latest ``` 2) Put a kernel `Image` at `./kernel/.build/arch/arm64/boot/Image` diff --git a/scripts/v9fs-entrypoint b/scripts/v9fs-entrypoint deleted file mode 100644 index 0070268..0000000 --- a/scripts/v9fs-entrypoint +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -umask 022 - -mkdir -p /workspaces/tmp -mkdir -p /home/v9fs-test - -# Default action: run the harness. -if [ "${1:-}" = "" ]; then - exec /usr/local/bin/v9fs-run-tests -fi - -exec "$@" - diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 1b65bc5..68a6cdb 100644 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -20,7 +20,7 @@ export FSDEV_PATH="/workspaces/share" export KERNELBUILD="${KERNELBUILD:-/workspaces/kernel/.build}" # Build initrd each run (cheap; keeps guest runner in sync). -/usr/local/bin/v9fs-build-initrd /home/v9fs-test/initrd.cpio +./scripts/v9fs-build-initrd /home/v9fs-test/initrd.cpio # Ask initrd to run guest tests, and propagate timestamp so the guest writes # guest.exitcode into the same logs// directory the host checks. From 8a6534c5b2f14b1352ac96574f039f222e7b0112 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:39:38 -0500 Subject: [PATCH 24/69] Make harness scripts executable Fix local/CI runs inside v9fs/docker by setting +x on scripts and qemu wrapper. Made-with: Cursor --- qemu.bash | 0 scripts/v9fs-build-initrd | 0 scripts/v9fs-guest-run | 0 scripts/v9fs-run-tests | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 qemu.bash mode change 100644 => 100755 scripts/v9fs-build-initrd mode change 100644 => 100755 scripts/v9fs-guest-run mode change 100644 => 100755 scripts/v9fs-run-tests diff --git a/qemu.bash b/qemu.bash old mode 100644 new mode 100755 diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd old mode 100644 new mode 100755 diff --git a/scripts/v9fs-guest-run b/scripts/v9fs-guest-run old mode 100644 new mode 100755 diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests old mode 100644 new mode 100755 From 6277b96376d1027c5ece9f41db46dc767a3619b0 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:40:08 -0500 Subject: [PATCH 25/69] Run v9fs/docker container as root for mounts Bind mounts under /workspaces/share require mount(2); run docker with --user 0:0 in local Makefile and CI/publish workflows. Made-with: Cursor --- .github/workflows/ci.yml | 1 + .github/workflows/linux-kernel-publish.yml | 1 + Makefile | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0aefa29..bdfa261 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ jobs: run: | mkdir -p "$GITHUB_WORKSPACE/tmp" docker run --rm --privileged \ + --user 0:0 \ -e KERNELBUILD=/workspaces/kernel/.build \ -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ diff --git a/.github/workflows/linux-kernel-publish.yml b/.github/workflows/linux-kernel-publish.yml index d2231c0..dcc6156 100644 --- a/.github/workflows/linux-kernel-publish.yml +++ b/.github/workflows/linux-kernel-publish.yml @@ -44,6 +44,7 @@ jobs: run: | mkdir -p "$GITHUB_WORKSPACE/tmp" "$GITHUB_WORKSPACE/kernel" docker run --rm --privileged \ + --user 0:0 \ -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ -v "$GITHUB_WORKSPACE/linux:/workspaces/linux" \ -v "$GITHUB_WORKSPACE/kernel:/workspaces/kernel" \ diff --git a/Makefile b/Makefile index c509631..826d660 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ docker-smoke: mkdir -p ./tmp ./kernel @echo "Place kernel Image at ./kernel/.build/arch/arm64/boot/Image (or download from release $(KERNEL_RELEASE))" docker run --rm --privileged \ + --user 0:0 \ -e KERNELBUILD=/workspaces/kernel/.build \ -v "$$(pwd):/home/v9fs-test/test" \ -v "$$(pwd)/kernel:/workspaces/kernel" \ From 3e4506608e1e08d05fbf9230c2ed28552c0448df Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:40:25 -0500 Subject: [PATCH 26/69] Initrd: locate busybox via PATH Use command -v busybox instead of assuming /bin/busybox (v9fs/docker image layout differs). Made-with: Cursor --- scripts/v9fs-build-initrd | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index cf863d0..e40d4e4 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -58,7 +58,13 @@ cat >"$work/etc/group" <<'EOF' root:x:0: EOF -cp /bin/busybox "$work/bin/busybox" +# Busybox provides core utils (sh, mount, etc.) inside initrd. +bb="$(command -v busybox || true)" +if [ -z "${bb}" ] || [ ! -x "${bb}" ]; then + echo "ERROR: busybox not found in container image" + exit 2 +fi +cp "${bb}" "$work/bin/busybox" for a in sh mount mkdir ln ls cat echo sleep uname ps kill date sed rm sync poweroff halt reboot; do ln -s /bin/busybox "$work/bin/$a" 2>/dev/null || true done From f5e8d894000cc968b8b1951b23b6f2072ac85b74 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:41:58 -0500 Subject: [PATCH 27/69] Initrd: build with u-root from v9fs/docker Use /home/v9fs-test/go/bin/u-root to generate a minimal initramfs (no busybox dependency) and embed our /init to mount 9p and run guest smoke. Made-with: Cursor --- scripts/v9fs-build-initrd | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index e40d4e4..52cf23b 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -5,21 +5,23 @@ out="${1:-/home/v9fs-test/initrd.cpio}" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT -mkdir -p "$work"/{bin,sbin,etc,proc,sys,dev,run,tmp,mnt/9} +u_root="${UROOT_BIN:-/home/v9fs-test/go/bin/u-root}" +if [ ! -x "$u_root" ]; then + echo "ERROR: u-root binary not found/executable at $u_root" + exit 2 +fi -cat >"$work/init" <<'EOF' +init="$work/init" +cat >"$init" <<'EOF' #!/bin/sh set -eu -export PATH=/bin:/sbin - mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev || true mkdir -p /run /tmp /mnt/9 -# Mount hostshare (virtio-9p) exported by QEMU. if ! mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then echo "initrd: ERROR: failed to mount hostshare on /mnt/9" fi @@ -27,8 +29,8 @@ fi cmdline="$(cat /proc/cmdline 2>/dev/null || true)" case "$cmdline" in *v9fs.run=1*) - tests="$(echo "$cmdline" | sed -n 's/.*v9fs.tests=\\([^ ]*\\).*/\\1/p')" - ts="$(echo "$cmdline" | sed -n 's/.*v9fs.ts=\\([^ ]*\\).*/\\1/p')" + tests="$(printf %s "$cmdline" | sed -n 's/.*v9fs.tests=\\([^ ]*\\).*/\\1/p')" + ts="$(printf %s "$cmdline" | sed -n 's/.*v9fs.ts=\\([^ ]*\\).*/\\1/p')" : "${tests:=smoke}" if [ -n "${ts:-}" ]; then export TIMESTAMP="$ts" @@ -49,26 +51,9 @@ esac echo "initrd up; interactive shell" exec sh EOF -chmod +x "$work/init" - -cat >"$work/etc/passwd" <<'EOF' -root:x:0:0:root:/root:/bin/sh -EOF -cat >"$work/etc/group" <<'EOF' -root:x:0: -EOF - -# Busybox provides core utils (sh, mount, etc.) inside initrd. -bb="$(command -v busybox || true)" -if [ -z "${bb}" ] || [ ! -x "${bb}" ]; then - echo "ERROR: busybox not found in container image" - exit 2 -fi -cp "${bb}" "$work/bin/busybox" -for a in sh mount mkdir ln ls cat echo sleep uname ps kill date sed rm sync poweroff halt reboot; do - ln -s /bin/busybox "$work/bin/$a" 2>/dev/null || true -done +chmod +x "$init" -(cd "$work" && find . -print0 | cpio --null -ov --format=newc) >"$out" +# Build a minimal initramfs with u-root (bundled busybox + core utilities). +"$u_root" -o "$out" -initcmd="/init" -files "$init" echo "Wrote initrd to $out" From afa43c95161c3cdaed2236312c6254a2a8310508 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:42:23 -0500 Subject: [PATCH 28/69] Initrd: overlay /init onto v9fs/docker base initrd Use u-root with -base /home/v9fs-test/initrd.cpio and -nocmd so we don't need the go toolchain; just overlay our /init to mount 9p and run the guest smoke. Made-with: Cursor --- scripts/v9fs-build-initrd | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 52cf23b..a76d657 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -11,6 +11,12 @@ if [ ! -x "$u_root" ]; then exit 2 fi +base="${UROOT_BASE:-/home/v9fs-test/initrd.cpio}" +if [ ! -f "$base" ]; then + echo "ERROR: base initrd not found at $base" + exit 2 +fi + init="$work/init" cat >"$init" <<'EOF' #!/bin/sh @@ -53,7 +59,7 @@ exec sh EOF chmod +x "$init" -# Build a minimal initramfs with u-root (bundled busybox + core utilities). -"$u_root" -o "$out" -initcmd="/init" -files "$init" +# Overlay our /init onto the base initrd, without building any new Go commands. +"$u_root" -base "$base" -nocmd -o "$out" -initcmd="/init" -files "$init" echo "Wrote initrd to $out" From 69bed9dc7803a2a6aa826f38878ddb368cea9221 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:44:12 -0500 Subject: [PATCH 29/69] Initrd overlay: keep base /bin/sh as default shell Pass -defaultsh=/bin/sh to u-root overlay to avoid gosh symlink errors when modifying the base initrd. Made-with: Cursor --- scripts/v9fs-build-initrd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index a76d657..300de0c 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -60,6 +60,7 @@ EOF chmod +x "$init" # Overlay our /init onto the base initrd, without building any new Go commands. -"$u_root" -base "$base" -nocmd -o "$out" -initcmd="/init" -files "$init" +# Also keep /bin/sh from the base archive as the default shell. +"$u_root" -base "$base" -nocmd -defaultsh="/bin/sh" -o "$out" -initcmd="/init" -files "$init" echo "Wrote initrd to $out" From 9e16186f5aa8ed5547381562e5df6b00fc3ca5f7 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:53:23 -0500 Subject: [PATCH 30/69] Initrd: replace /init in base initrd via Python newc Stop using u-root overlay (fails in v9fs/docker). Instead, rewrite the base /home/v9fs-test/initrd.cpio in pure Python to replace the init entry with our guest runner init. Made-with: Cursor --- scripts/v9fs-build-initrd | 153 +++++++++++++++++++++++++++++++++++--- 1 file changed, 144 insertions(+), 9 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 300de0c..dbbd40c 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -5,12 +5,7 @@ out="${1:-/home/v9fs-test/initrd.cpio}" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT -u_root="${UROOT_BIN:-/home/v9fs-test/go/bin/u-root}" -if [ ! -x "$u_root" ]; then - echo "ERROR: u-root binary not found/executable at $u_root" - exit 2 -fi - +# We run inside the `v9fs/docker` image, which provides a base initrd at this path. base="${UROOT_BASE:-/home/v9fs-test/initrd.cpio}" if [ ! -f "$base" ]; then echo "ERROR: base initrd not found at $base" @@ -59,8 +54,148 @@ exec sh EOF chmod +x "$init" -# Overlay our /init onto the base initrd, without building any new Go commands. -# Also keep /bin/sh from the base archive as the default shell. -"$u_root" -base "$base" -nocmd -defaultsh="/bin/sh" -o "$out" -initcmd="/init" -files "$init" +# Overlay our /init onto the base initrd using a tiny Python newc (cpio) rewriter. +# This avoids needing `cpio`, `busybox`, or a working u-root build toolchain. +python3 - "$base" "$init" "$out" <<'PY' +import os, sys, stat, time + +base_path, init_path, out_path = sys.argv[1:4] + +def align4(x: int) -> int: + return (x + 3) & ~3 + +def read_exact(f, n: int) -> bytes: + b = f.read(n) + if len(b) != n: + raise EOFError(f"short read: wanted {n}, got {len(b)}") + return b + +def parse_hex(b: bytes) -> int: + return int(b.decode("ascii"), 16) + +def write_header(w, **fields): + # newc header fields are 8 hex bytes each (except magic) + parts = [ + b"070701", + f"{fields['ino']:08x}".encode(), + f"{fields['mode']:08x}".encode(), + f"{fields['uid']:08x}".encode(), + f"{fields['gid']:08x}".encode(), + f"{fields['nlink']:08x}".encode(), + f"{fields['mtime']:08x}".encode(), + f"{fields['filesize']:08x}".encode(), + f"{fields['devmajor']:08x}".encode(), + f"{fields['devminor']:08x}".encode(), + f"{fields['rdevmajor']:08x}".encode(), + f"{fields['rdevminor']:08x}".encode(), + f"{fields['namesize']:08x}".encode(), + f"{fields['check']:08x}".encode(), + ] + w.write(b"".join(parts)) + +def iter_newc_entries(f): + while True: + hdr = read_exact(f, 110) + if hdr[:6] != b"070701": + raise ValueError(f"bad magic: {hdr[:6]!r}") + + ino = parse_hex(hdr[6:14]) + mode = parse_hex(hdr[14:22]) + uid = parse_hex(hdr[22:30]) + gid = parse_hex(hdr[30:38]) + nlink = parse_hex(hdr[38:46]) + mtime = parse_hex(hdr[46:54]) + filesize = parse_hex(hdr[54:62]) + devmajor = parse_hex(hdr[62:70]) + devminor = parse_hex(hdr[70:78]) + rdevmajor = parse_hex(hdr[78:86]) + rdevminor = parse_hex(hdr[86:94]) + namesize = parse_hex(hdr[94:102]) + check = parse_hex(hdr[102:110]) + + name = read_exact(f, namesize) + # name includes trailing NUL + if not name.endswith(b"\x00"): + raise ValueError("name missing NUL terminator") + name_str = name[:-1].decode("utf-8", "replace") + + # pad after name to 4-byte boundary from start of header + pad = align4(110 + namesize) - (110 + namesize) + if pad: + read_exact(f, pad) + + data = read_exact(f, filesize) if filesize else b"" + pad = align4(filesize) - filesize + if pad: + read_exact(f, pad) + + yield { + "ino": ino, "mode": mode, "uid": uid, "gid": gid, "nlink": nlink, "mtime": mtime, + "filesize": filesize, "devmajor": devmajor, "devminor": devminor, + "rdevmajor": rdevmajor, "rdevminor": rdevminor, "namesize": namesize, "check": check, + "name": name_str, "data": data, + } + + if name_str == "TRAILER!!!": + break + +def write_entry(w, name: str, mode: int, uid: int, gid: int, data: bytes): + if not name.endswith("\x00"): + name_bytes = name.encode("utf-8") + b"\x00" + else: + name_bytes = name.encode("utf-8") + namesize = len(name_bytes) + write_header( + w, + ino=0, + mode=mode, + uid=uid, + gid=gid, + nlink=1, + mtime=int(time.time()), + filesize=len(data), + devmajor=0, devminor=0, rdevmajor=0, rdevminor=0, + namesize=namesize, + check=0, + ) + w.write(name_bytes) + # pad to 4-byte boundary after name + pad = align4(110 + namesize) - (110 + namesize) + if pad: + w.write(b"\x00" * pad) + if data: + w.write(data) + pad = align4(len(data)) - len(data) + if pad: + w.write(b"\x00" * pad) + +def main(): + init_data = open(init_path, "rb").read() + init_mode = stat.S_IFREG | 0o755 + + replaced = False + with open(base_path, "rb") as rf, open(out_path, "wb") as wf: + for ent in iter_newc_entries(rf): + name = ent["name"] + if name == "TRAILER!!!": + break + if name in ("init", "/init"): + write_entry(wf, "init", init_mode, 0, 0, init_data) + replaced = True + else: + write_entry(wf, name, ent["mode"], ent["uid"], ent["gid"], ent["data"]) + + if not replaced: + # If base initrd didn't have init, add it before trailer. + write_entry(wf, "init", init_mode, 0, 0, init_data) + + # trailer + write_entry(wf, "TRAILER!!!", stat.S_IFREG | 0o644, 0, 0, b"") + + os.chmod(out_path, 0o644) + +if __name__ == "__main__": + main() +PY echo "Wrote initrd to $out" From fa51653339d5cfd04721a501af92702d3c773093 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:53:42 -0500 Subject: [PATCH 31/69] Initrd rewrite: tolerate missing trailer EOF Handle base initrd archives that end without a TRAILER!!! record by treating EOF as end-of-archive. Made-with: Cursor --- scripts/v9fs-build-initrd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index dbbd40c..ef5c81d 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -95,7 +95,11 @@ def write_header(w, **fields): def iter_newc_entries(f): while True: - hdr = read_exact(f, 110) + try: + hdr = read_exact(f, 110) + except EOFError: + # Some initrds may omit a TRAILER!!! record and just end. + break if hdr[:6] != b"070701": raise ValueError(f"bad magic: {hdr[:6]!r}") From ecdc30793e827a6a2a68c9ccec2ba32639cf2f6e Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:55:52 -0500 Subject: [PATCH 32/69] Fix initrd overlay: never overwrite base initrd Write patched initrd to /home/v9fs-test/initrd.patched.cpio (temp + rename) and have v9fs-run-tests point QEMU at it, avoiding truncating the base initrd while reading. Made-with: Cursor --- scripts/v9fs-build-initrd | 6 ++++-- scripts/v9fs-run-tests | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index ef5c81d..0437c4e 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -out="${1:-/home/v9fs-test/initrd.cpio}" +out="${1:-/home/v9fs-test/initrd.patched.cpio}" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT @@ -178,7 +178,8 @@ def main(): init_mode = stat.S_IFREG | 0o755 replaced = False - with open(base_path, "rb") as rf, open(out_path, "wb") as wf: + tmp_out = out_path + ".tmp" + with open(base_path, "rb") as rf, open(tmp_out, "wb") as wf: for ent in iter_newc_entries(rf): name = ent["name"] if name == "TRAILER!!!": @@ -196,6 +197,7 @@ def main(): # trailer write_entry(wf, "TRAILER!!!", stat.S_IFREG | 0o644, 0, 0, b"") + os.replace(tmp_out, out_path) os.chmod(out_path, 0o644) if __name__ == "__main__": diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 68a6cdb..5edd7b7 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -20,7 +20,8 @@ export FSDEV_PATH="/workspaces/share" export KERNELBUILD="${KERNELBUILD:-/workspaces/kernel/.build}" # Build initrd each run (cheap; keeps guest runner in sync). -./scripts/v9fs-build-initrd /home/v9fs-test/initrd.cpio +export INITRD="/home/v9fs-test/initrd.patched.cpio" +./scripts/v9fs-build-initrd "${INITRD}" # Ask initrd to run guest tests, and propagate timestamp so the guest writes # guest.exitcode into the same logs// directory the host checks. From 8f4392983eb855c32100a87a39811cf99c45428c Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 11:59:27 -0500 Subject: [PATCH 33/69] Use u-root busybox (bb) applets in initrd and guest v9fs/docker base initrd only provides /bbin/bb; use it for mount/sed/ls/sync/poweroff and basic filesystem ops so the guest smoke can run. Made-with: Cursor --- scripts/v9fs-build-initrd | 25 ++++++++++++++----------- scripts/v9fs-guest-run | 33 ++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 0437c4e..82bd9b1 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -17,21 +17,24 @@ cat >"$init" <<'EOF' #!/bin/sh set -eu -mount -t proc proc /proc -mount -t sysfs sysfs /sys -mount -t devtmpfs devtmpfs /dev || true +BB=/bbin/bb +bb() { "$BB" "$@"; } -mkdir -p /run /tmp /mnt/9 +bb mount -t proc proc /proc +bb mount -t sysfs sysfs /sys +bb mount -t devtmpfs devtmpfs /dev || true -if ! mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then +bb mkdir -p /run /tmp /mnt/9 + +if ! bb mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then echo "initrd: ERROR: failed to mount hostshare on /mnt/9" fi -cmdline="$(cat /proc/cmdline 2>/dev/null || true)" +cmdline="$(bb cat /proc/cmdline 2>/dev/null || true)" case "$cmdline" in *v9fs.run=1*) - tests="$(printf %s "$cmdline" | sed -n 's/.*v9fs.tests=\\([^ ]*\\).*/\\1/p')" - ts="$(printf %s "$cmdline" | sed -n 's/.*v9fs.ts=\\([^ ]*\\).*/\\1/p')" + tests="$(printf %s "$cmdline" | bb sed -n 's/.*v9fs.tests=\\([^ ]*\\).*/\\1/p')" + ts="$(printf %s "$cmdline" | bb sed -n 's/.*v9fs.ts=\\([^ ]*\\).*/\\1/p')" : "${tests:=smoke}" if [ -n "${ts:-}" ]; then export TIMESTAMP="$ts" @@ -42,10 +45,10 @@ case "$cmdline" in /bin/sh /mnt/9/test/scripts/v9fs-guest-run "$tests" || true else echo "initrd: missing /mnt/9/test/scripts/v9fs-guest-run" - ls -la /mnt/9/test/scripts 2>/dev/null || true + bb ls -la /mnt/9/test/scripts 2>/dev/null || true fi - sync 2>/dev/null || true - poweroff -f || halt -f || reboot -f + bb sync 2>/dev/null || true + bb poweroff -f || bb halt -f || bb reboot -f ;; esac diff --git a/scripts/v9fs-guest-run b/scripts/v9fs-guest-run index 3ddbece..f22d2f7 100755 --- a/scripts/v9fs-guest-run +++ b/scripts/v9fs-guest-run @@ -7,9 +7,12 @@ repo="/mnt/9/test" ts="${TIMESTAMP:-$(date +%s)}" logdir="$repo/logs/$ts" -mkdir -p "$logdir" -rm -f "$repo/logs/current" -ln -s "$ts" "$repo/logs/current" +BB=/bbin/bb +bb() { "$BB" "$@"; } + +bb mkdir -p "$logdir" +bb rm -f "$repo/logs/current" +bb ln -s "$ts" "$repo/logs/current" guestlog="$logdir/guest.log" rcfile="$logdir/guest.exitcode" @@ -17,40 +20,40 @@ rcfile="$logdir/guest.exitcode" exec >"$guestlog" 2>&1 echo "GUEST: tests=$tests ts=$ts" -echo "GUEST: mounted /mnt/9: $(mount | grep ' /mnt/9 ' || true)" +echo "GUEST: mounted /mnt/9: $(bb mount | bb grep ' /mnt/9 ' || true)" tmpdir="/mnt/9/tmpdir" -mkdir -p "$tmpdir" +bb mkdir -p "$tmpdir" rc=0 case "$tests" in smoke|*) t="$tmpdir/smoke.$$" - mkdir -p "$t" + bb mkdir -p "$t" echo "GUEST: write/read basic file" printf 'hello-9p\n' >"$t/hello.txt" || rc=1 - [ "$(cat "$t/hello.txt" 2>/dev/null || true)" = "hello-9p" ] || rc=1 + [ "$(bb cat "$t/hello.txt" 2>/dev/null || true)" = "hello-9p" ] || rc=1 echo "GUEST: rename + append" - mv "$t/hello.txt" "$t/renamed.txt" || rc=1 + bb mv "$t/hello.txt" "$t/renamed.txt" || rc=1 printf 'more\n' >>"$t/renamed.txt" || rc=1 - grep -q 'more' "$t/renamed.txt" || rc=1 + bb grep -q 'more' "$t/renamed.txt" || rc=1 echo "GUEST: mkdir/list/rmdir" - mkdir -p "$t/dirA" || rc=1 - ls -la "$t" >/dev/null 2>&1 || rc=1 - rmdir "$t/dirA" || rc=1 + bb mkdir -p "$t/dirA" || rc=1 + bb ls -la "$t" >/dev/null 2>&1 || rc=1 + bb rmdir "$t/dirA" || rc=1 echo "GUEST: unlink cleanup" - rm -f "$t/renamed.txt" || rc=1 - rmdir "$t" || rc=1 + bb rm -f "$t/renamed.txt" || rc=1 + bb rmdir "$t" || rc=1 ;; esac echo "$rc" >"$rcfile" 2>/dev/null || true -sync 2>/dev/null || true +bb sync 2>/dev/null || true echo "GUEST: done rc=$rc" exit "$rc" From ecb4f2216aaa02da9c09169791cf08ce1c83f23a Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:02:15 -0500 Subject: [PATCH 34/69] Switch to u-root init + /uinit for guest run Patch base initrd by rewriting /init -> bbin/init and injecting an executable /uinit script. Then boot with uroot.uinitcmd=/uinit so we can mount 9p and run v9fs-guest-run without relying on a full userspace. Made-with: Cursor --- scripts/v9fs-build-initrd | 59 ++++++++++++++++++++++----------------- scripts/v9fs-run-tests | 2 +- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 82bd9b1..a207a68 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -12,54 +12,49 @@ if [ ! -f "$base" ]; then exit 2 fi -init="$work/init" -cat >"$init" <<'EOF' +uinit="$work/uinit" +cat >"$uinit" <<'EOF' #!/bin/sh set -eu -BB=/bbin/bb -bb() { "$BB" "$@"; } +mkdir -p /run /tmp /mnt/9 -bb mount -t proc proc /proc -bb mount -t sysfs sysfs /sys -bb mount -t devtmpfs devtmpfs /dev || true - -bb mkdir -p /run /tmp /mnt/9 - -if ! bb mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then - echo "initrd: ERROR: failed to mount hostshare on /mnt/9" +# `v9fs/docker` initrd is u-root based. With `/init` pointing at `bbin/init`, +# this script is invoked as u-root's uinit (see `uroot.uinitcmd=/uinit`). +if ! mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then + echo "uinit: ERROR: failed to mount hostshare on /mnt/9" fi -cmdline="$(bb cat /proc/cmdline 2>/dev/null || true)" +cmdline="$(cat /proc/cmdline 2>/dev/null || true)" case "$cmdline" in *v9fs.run=1*) - tests="$(printf %s "$cmdline" | bb sed -n 's/.*v9fs.tests=\\([^ ]*\\).*/\\1/p')" - ts="$(printf %s "$cmdline" | bb sed -n 's/.*v9fs.ts=\\([^ ]*\\).*/\\1/p')" + tests="$(printf %s "$cmdline" | sed -n 's/.*v9fs.tests=\\([^ ]*\\).*/\\1/p')" + ts="$(printf %s "$cmdline" | sed -n 's/.*v9fs.ts=\\([^ ]*\\).*/\\1/p')" : "${tests:=smoke}" if [ -n "${ts:-}" ]; then export TIMESTAMP="$ts" fi - echo "initrd: running guest tests tests=$tests ts=${TIMESTAMP:-unset}" + echo "uinit: running guest tests tests=$tests ts=${TIMESTAMP:-unset}" if [ -x /mnt/9/test/scripts/v9fs-guest-run ]; then /bin/sh /mnt/9/test/scripts/v9fs-guest-run "$tests" || true else - echo "initrd: missing /mnt/9/test/scripts/v9fs-guest-run" - bb ls -la /mnt/9/test/scripts 2>/dev/null || true + echo "uinit: missing /mnt/9/test/scripts/v9fs-guest-run" + ls -la /mnt/9/test/scripts 2>/dev/null || true fi - bb sync 2>/dev/null || true - bb poweroff -f || bb halt -f || bb reboot -f + sync 2>/dev/null || true + poweroff -f || halt -f || reboot -f ;; esac -echo "initrd up; interactive shell" +echo "uinit: interactive shell" exec sh EOF -chmod +x "$init" +chmod +x "$uinit" # Overlay our /init onto the base initrd using a tiny Python newc (cpio) rewriter. # This avoids needing `cpio`, `busybox`, or a working u-root build toolchain. -python3 - "$base" "$init" "$out" <<'PY' +python3 - "$base" "$uinit" "$out" <<'PY' import os, sys, stat, time base_path, init_path, out_path = sys.argv[1:4] @@ -176,11 +171,16 @@ def write_entry(w, name: str, mode: int, uid: int, gid: int, data: bytes): if pad: w.write(b"\x00" * pad) +def write_symlink(w, name: str, target: str, uid: int = 0, gid: int = 0): + mode = stat.S_IFLNK | 0o777 + write_entry(w, name, mode, uid, gid, target.encode("utf-8")) + def main(): init_data = open(init_path, "rb").read() - init_mode = stat.S_IFREG | 0o755 + uinit_mode = stat.S_IFREG | 0o755 replaced = False + uinit_written = False tmp_out = out_path + ".tmp" with open(base_path, "rb") as rf, open(tmp_out, "wb") as wf: for ent in iter_newc_entries(rf): @@ -188,14 +188,21 @@ def main(): if name == "TRAILER!!!": break if name in ("init", "/init"): - write_entry(wf, "init", init_mode, 0, 0, init_data) + # Replace base init symlink (cpud) with u-root init. + write_symlink(wf, "init", "bbin/init") replaced = True + elif name in ("uinit", "/uinit"): + write_entry(wf, "uinit", uinit_mode, 0, 0, init_data) + uinit_written = True else: write_entry(wf, name, ent["mode"], ent["uid"], ent["gid"], ent["data"]) if not replaced: # If base initrd didn't have init, add it before trailer. - write_entry(wf, "init", init_mode, 0, 0, init_data) + write_symlink(wf, "init", "bbin/init") + + if not uinit_written: + write_entry(wf, "uinit", uinit_mode, 0, 0, init_data) # trailer write_entry(wf, "TRAILER!!!", stat.S_IFREG | 0o644, 0, 0, b"") diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 5edd7b7..0d52eff 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -25,7 +25,7 @@ export INITRD="/home/v9fs-test/initrd.patched.cpio" # Ask initrd to run guest tests, and propagate timestamp so the guest writes # guest.exitcode into the same logs// directory the host checks. -export EXTRA_APPEND="v9fs.run=1 v9fs.tests=${tests} v9fs.ts=${TIMESTAMP}" +export EXTRA_APPEND="v9fs.run=1 v9fs.tests=${tests} v9fs.ts=${TIMESTAMP} uroot.uinitcmd=/uinit" /home/v9fs-test/test/qemu.bash From 83b30514142bc612318371068048cb72766c6b28 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:05:38 -0500 Subject: [PATCH 35/69] Install guest runner as /bin/uinit (u-root default) u-root bb init looks for /bin/uinit by default; write our guest test bootstrap there and drop the (incorrect) uinitcmd kernel parameter. Made-with: Cursor --- scripts/v9fs-build-initrd | 10 +++++----- scripts/v9fs-run-tests | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index a207a68..d98e696 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -19,8 +19,8 @@ set -eu mkdir -p /run /tmp /mnt/9 -# `v9fs/docker` initrd is u-root based. With `/init` pointing at `bbin/init`, -# this script is invoked as u-root's uinit (see `uroot.uinitcmd=/uinit`). +# `v9fs/docker` initrd is u-root based. We install this script as `/bin/uinit`, +# which u-root init executes automatically. if ! mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then echo "uinit: ERROR: failed to mount hostshare on /mnt/9" fi @@ -191,8 +191,8 @@ def main(): # Replace base init symlink (cpud) with u-root init. write_symlink(wf, "init", "bbin/init") replaced = True - elif name in ("uinit", "/uinit"): - write_entry(wf, "uinit", uinit_mode, 0, 0, init_data) + elif name in ("bin/uinit", "/bin/uinit"): + write_entry(wf, "bin/uinit", uinit_mode, 0, 0, init_data) uinit_written = True else: write_entry(wf, name, ent["mode"], ent["uid"], ent["gid"], ent["data"]) @@ -202,7 +202,7 @@ def main(): write_symlink(wf, "init", "bbin/init") if not uinit_written: - write_entry(wf, "uinit", uinit_mode, 0, 0, init_data) + write_entry(wf, "bin/uinit", uinit_mode, 0, 0, init_data) # trailer write_entry(wf, "TRAILER!!!", stat.S_IFREG | 0o644, 0, 0, b"") diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 0d52eff..5edd7b7 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -25,7 +25,7 @@ export INITRD="/home/v9fs-test/initrd.patched.cpio" # Ask initrd to run guest tests, and propagate timestamp so the guest writes # guest.exitcode into the same logs// directory the host checks. -export EXTRA_APPEND="v9fs.run=1 v9fs.tests=${tests} v9fs.ts=${TIMESTAMP} uroot.uinitcmd=/uinit" +export EXTRA_APPEND="v9fs.run=1 v9fs.tests=${tests} v9fs.ts=${TIMESTAMP}" /home/v9fs-test/test/qemu.bash From 79ab26a32da462422ba2318846f98c0cd2244abc Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:10:12 -0500 Subject: [PATCH 36/69] uinit: use /bbin/bb, parse cmdline safely, no interactive shell Make /bin/uinit a minimal non-interactive runner: parse v9fs.* flags without sed, mount 9p via u-root bb applets, run v9fs-guest-run, sync, and power off. This avoids missing applets and stops the guest from idling into an interactive prompt. Made-with: Cursor --- scripts/v9fs-build-initrd | 74 ++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index d98e696..ba0a38d 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -19,36 +19,54 @@ set -eu mkdir -p /run /tmp /mnt/9 -# `v9fs/docker` initrd is u-root based. We install this script as `/bin/uinit`, -# which u-root init executes automatically. -if ! mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then - echo "uinit: ERROR: failed to mount hostshare on /mnt/9" +bb="/bbin/bb" + +say() { + # best-effort: also mirror to kernel log if console is weird + echo "$@" || true + [ -c /dev/kmsg ] && echo "$@" >/dev/kmsg 2>/dev/null || true +} + +cmdline="$(/dev/null || true)" +run=0 +tests="smoke" +ts="" +for w in $cmdline; do + case "$w" in + v9fs.run=1) run=1 ;; + v9fs.tests=*) tests="${w#v9fs.tests=}" ;; + v9fs.ts=*) ts="${w#v9fs.ts=}" ;; + esac +done + +if [ "$run" != "1" ]; then + say "uinit: v9fs.run not set; powering off" + "$bb" poweroff -f || "$bb" halt -f || "$bb" reboot -f + exit 0 fi -cmdline="$(cat /proc/cmdline 2>/dev/null || true)" -case "$cmdline" in - *v9fs.run=1*) - tests="$(printf %s "$cmdline" | sed -n 's/.*v9fs.tests=\\([^ ]*\\).*/\\1/p')" - ts="$(printf %s "$cmdline" | sed -n 's/.*v9fs.ts=\\([^ ]*\\).*/\\1/p')" - : "${tests:=smoke}" - if [ -n "${ts:-}" ]; then - export TIMESTAMP="$ts" - fi - - echo "uinit: running guest tests tests=$tests ts=${TIMESTAMP:-unset}" - if [ -x /mnt/9/test/scripts/v9fs-guest-run ]; then - /bin/sh /mnt/9/test/scripts/v9fs-guest-run "$tests" || true - else - echo "uinit: missing /mnt/9/test/scripts/v9fs-guest-run" - ls -la /mnt/9/test/scripts 2>/dev/null || true - fi - sync 2>/dev/null || true - poweroff -f || halt -f || reboot -f - ;; -esac - -echo "uinit: interactive shell" -exec sh +if [ -n "${ts:-}" ]; then + export TIMESTAMP="$ts" +fi + +say "uinit: starting tests=$tests ts=${TIMESTAMP:-unset}" + +if ! "$bb" mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then + say "uinit: ERROR: failed to mount hostshare on /mnt/9" + "$bb" sync 2>/dev/null || true + "$bb" poweroff -f || "$bb" halt -f || "$bb" reboot -f + exit 1 +fi + +if [ -x /mnt/9/test/scripts/v9fs-guest-run ]; then + /bin/sh /mnt/9/test/scripts/v9fs-guest-run "$tests" || true +else + say "uinit: missing /mnt/9/test/scripts/v9fs-guest-run" + "$bb" ls -la /mnt/9/test/scripts 2>/dev/null || true +fi + +"$bb" sync 2>/dev/null || true +"$bb" poweroff -f || "$bb" halt -f || "$bb" reboot -f EOF chmod +x "$uinit" From 384ad1800d1f05f819b34d06d65ecc6551b8946e Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:10:51 -0500 Subject: [PATCH 37/69] Docs: add UROOT.md and CPU.md lookaside notes Capture u-root init/uinit behavior and cpu/cpud flags from source so we have a local reference that matches the actual implementation we boot in v9fs/docker. Made-with: Cursor --- CPU.md | 45 ++++++++++++++++++++++++++++++++++++++++++ UROOT.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 CPU.md create mode 100644 UROOT.md diff --git a/CPU.md b/CPU.md new file mode 100644 index 0000000..d80d382 --- /dev/null +++ b/CPU.md @@ -0,0 +1,45 @@ +# CPU.md + +Lookaside notes for **u-root/cpu** (`cpu` + `cpud`) as it relates to this repo and the `v9fs/docker` initrd. + +Per `AGENTS.md`, treat **source code** as the authority. + +## What `cpud` is + +`cpud` is the “daemon side” of a Plan9-inspired `cpu` session. It can run: + +- **as PID 1** (init + daemon) +- **as a daemon** (listening server that forks per-session cpuds) +- **as a per-session “remote”** process + +Source of truth: + +- `u-root/cpu` `cmds/cpud/main_linux.go` and `cmds/cpud/cpuddoc.go` + +## `cpud` flags (server mode) + +From `cmds/cpud/main_linux.go` and `cmds/cpud/cpuddoc.go`: + +- `-hk`: host key file (host key) +- `-pk`: public key file (default `key.pub`) +- `-sp`: listen port (default `17010`) +- `-net`: network (default `tcp`) +- `-d`: debug prints +- `-klog`: log to kernel log instead of stdout +- `-register` / `-registerTO`: optional controller registration +- `-sleepBeforeServing`: delay before serving (useful when running as init) + +## `cpud` “remote” mode and argument rules + +In remote mode (invoked as `cpud -remote ...`) the accepted flag set is intentionally smaller, and the program expects remaining args to be the command to run. + +Source of truth: + +- `cmds/cpud/main_linux.go` (`if len(os.Args)>1 && os.Args[1] == "-remote"...` and the custom FlagSet) + +## Relevance to `v9fs/docker` + +The `v9fs/docker` base initrd’s `/init` is a symlink to `bbin/cpud` (and `bbin/cpud` is an applet of the `bb` binary). + +For this repo’s harness, we are **not** depending on `cpu/cpud` features to run tests yet; we only rely on the u-root init sequence and the availability of `/bbin/bb` applets to implement `/bin/uinit` and mount 9p. + diff --git a/UROOT.md b/UROOT.md new file mode 100644 index 0000000..72cb5c6 --- /dev/null +++ b/UROOT.md @@ -0,0 +1,59 @@ +# UROOT.md + +Lookaside notes for how **u-root init** behaves in the `v9fs/docker` initrd we boot in this repo. + +Per `AGENTS.md`, this is based on **implementation evidence**, not on secondary docs. + +## What the `v9fs/docker` initrd actually contains + +From inspecting `/home/v9fs-test/initrd.cpio` inside the `ghcr.io/v9fs/docker:latest` image: + +- `init` is a **symlink** to `bbin/cpud` +- `bbin/cpud`, `bbin/gosh`, and `bbin/init` are **symlinks** to `bb` +- `bbin/bb` is the big u-root “busybox” ELF containing applets (including `init`) +- `bin/sh` and `bin/defaultsh` are **symlinks** to `../bbin/gosh` +- there is **no** `bin/mount`, `bin/sed`, etc as standalone files; those operations must come from u-root applets (usually invoked via `/bbin/bb ...`) + +This is why any init/uinit script we provide must avoid assuming a traditional userspace. + +## How u-root decides what to run after `/init` + +The u-root `init` implementation (upstream) tries executables in order: + +- `/inito` (original init, if present) +- `/bbin/uinit` +- `/bin/uinit` +- `/buildbin/uinit` +- `/bin/defaultsh` +- `/bin/sh` + +Source of truth: + +- u-root `init` linux flow: `cmds/core/init/init_linux.go` + - `osInitGo()` builds the command list above. + - `cmdline.GetUinitArgs()` reads `uroot.uinitargs`. + +## Kernel command-line flags u-root reads + +u-root parses `/proc/cmdline` and provides helpers for specific keys: + +- **`uroot.uinitargs`**: a shell-lexed string turned into argv for the `uinit` program. + - Implementation: `pkg/cmdline.(*CmdLine).GetUinitArgs()` calls `shlex.Argv(uinitargs)`. +- **`uroot.initflags`**: a string parsed as a flag-map (used for init-time options like `systemd`). + - Implementation: `pkg/cmdline.(*CmdLine).GetInitFlagMap()`. + +Source of truth: + +- `pkg/cmdline/cmdline.go` (functions `GetUinitArgs`, `GetInitFlagMap`, parsing rules) + +## How we hook guest tests into this + +We patch the base initrd to include an executable **`/bin/uinit`** that: + +- uses `/bbin/bb` applets for `mount`, `sync`, `poweroff`, etc +- mounts the QEMU 9p export (`hostshare`) at `/mnt/9` +- runs `/mnt/9/test/scripts/v9fs-guest-run ` +- syncs and powers off + +This avoids rebuilding u-root and avoids relying on external tools inside the initrd. + From 08a650a8e638a5f7a23f170eca33485b2469cb78 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:12:54 -0500 Subject: [PATCH 38/69] uinit: redirect stdout/stderr to /dev/console Ensure uinit logs show up on the serial console captured in qemu.log for CI/local debugging. Made-with: Cursor --- scripts/v9fs-build-initrd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index ba0a38d..adba66a 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -17,6 +17,9 @@ cat >"$uinit" <<'EOF' #!/bin/sh set -eu +# Force output to the serial console that QEMU captures. +exec >/dev/console 2>&1 + mkdir -p /run /tmp /mnt/9 bb="/bbin/bb" From 0806e2e0bf0f331cc6f193ed71dd879e8e23d3ec Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:18:44 -0500 Subject: [PATCH 39/69] Run guest tests via u-root cpu (cpud) over hostfwd Switch harness back to the proven u-root/cpu format: boot with the image initrd (cpud as PID1), forward port 17010, wait for cpu connectivity, invoke v9fs-guest-run via cpu, and read guest.exitcode from shared logs. Made-with: Cursor --- qemu.bash | 2 +- scripts/v9fs-guest-run | 49 +++++++++++++++++++++------------------ scripts/v9fs-run-tests | 52 +++++++++++++++++++++++++++--------------- 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/qemu.bash b/qemu.bash index da37855..6abd190 100755 --- a/qemu.bash +++ b/qemu.bash @@ -34,7 +34,7 @@ exec ${QEMU} \ -object rng-random,filename=/dev/urandom,id=rng0 \ -device virtio-rng-pci,rng=rng0 \ -device virtio-net-pci,netdev=n1 \ - -netdev user,id=n1 \ + -netdev user,id=n1,hostfwd=tcp:127.0.0.1:17010-:17010 \ -serial "file:${LOG}" \ -fsdev "local,security_model=none,writeout=immediate,id=fsdev0,path=${FSDEV_PATH}" \ -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare \ diff --git a/scripts/v9fs-guest-run b/scripts/v9fs-guest-run index f22d2f7..352761c 100755 --- a/scripts/v9fs-guest-run +++ b/scripts/v9fs-guest-run @@ -2,58 +2,63 @@ set -eu tests="${1:-smoke}" +type="${2:-ci}" +check="${3:-1}" -repo="/mnt/9/test" +# When invoked via `cpu`, we want to write logs into the host-mounted repo. +repo="/mnt/9/workspaces/share/test" ts="${TIMESTAMP:-$(date +%s)}" logdir="$repo/logs/$ts" -BB=/bbin/bb -bb() { "$BB" "$@"; } - -bb mkdir -p "$logdir" -bb rm -f "$repo/logs/current" -bb ln -s "$ts" "$repo/logs/current" +mkdir -p "$logdir" +rm -f "$repo/logs/current" +ln -s "$ts" "$repo/logs/current" guestlog="$logdir/guest.log" rcfile="$logdir/guest.exitcode" exec >"$guestlog" 2>&1 -echo "GUEST: tests=$tests ts=$ts" -echo "GUEST: mounted /mnt/9: $(bb mount | bb grep ' /mnt/9 ' || true)" +echo "GUEST: TYPE=$type TESTS=$tests CHECK=$check" +echo "GUEST: repo=$repo" +echo "GUEST: mounted /mnt/9: $(mount | grep ' /mnt/9 ' || true)" -tmpdir="/mnt/9/tmpdir" -bb mkdir -p "$tmpdir" +tmpdir="/mnt/9/workspaces/share/tmpdir" +mkdir -p "$tmpdir" rc=0 case "$tests" in - smoke|*) + smoke) t="$tmpdir/smoke.$$" - bb mkdir -p "$t" + mkdir -p "$t" echo "GUEST: write/read basic file" printf 'hello-9p\n' >"$t/hello.txt" || rc=1 - [ "$(bb cat "$t/hello.txt" 2>/dev/null || true)" = "hello-9p" ] || rc=1 + [ "$(cat "$t/hello.txt" 2>/dev/null || true)" = "hello-9p" ] || rc=1 echo "GUEST: rename + append" - bb mv "$t/hello.txt" "$t/renamed.txt" || rc=1 + mv "$t/hello.txt" "$t/renamed.txt" || rc=1 printf 'more\n' >>"$t/renamed.txt" || rc=1 - bb grep -q 'more' "$t/renamed.txt" || rc=1 + grep -q 'more' "$t/renamed.txt" || rc=1 echo "GUEST: mkdir/list/rmdir" - bb mkdir -p "$t/dirA" || rc=1 - bb ls -la "$t" >/dev/null 2>&1 || rc=1 - bb rmdir "$t/dirA" || rc=1 + mkdir -p "$t/dirA" || rc=1 + ls -la "$t" >/dev/null 2>&1 || rc=1 + rmdir "$t/dirA" || rc=1 echo "GUEST: unlink cleanup" - bb rm -f "$t/renamed.txt" || rc=1 - bb rmdir "$t" || rc=1 + rm -f "$t/renamed.txt" || rc=1 + rmdir "$t" || rc=1 + ;; + *) + echo "GUEST: unknown tests=$tests" + rc=2 ;; esac echo "$rc" >"$rcfile" 2>/dev/null || true -bb sync 2>/dev/null || true +sync 2>/dev/null || true echo "GUEST: done rc=$rc" exit "$rc" diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 5edd7b7..cd905da 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -2,49 +2,65 @@ set -euo pipefail tests="${1:-smoke}" -export TIMESTAMP="${TIMESTAMP:-$(date +%s)}" +type="${2:-ci}" +check="${3:-1}" + +# Ensure artifacts created by root inside Docker are readable by the +# GitHub Actions runner user when uploading `logs/` as an artifact. +umask 022 +cpu_bin="${CPU_BIN:-/home/v9fs-test/go/bin/cpu}" +cpu_key="${CPU_KEY:-/home/v9fs-test/.ssh/identity}" + +export TIMESTAMP="${TIMESTAMP:-$(date +%s)}" mkdir -p "logs/${TIMESTAMP}" export QEMULOG="logs/${TIMESTAMP}/qemu.log" export PIDFILE="/home/v9fs-test/qemu.pid" -# Make a stable export root for 9p and ensure we have a temp directory to export. +# Stable export root so 9p sees our bind mounts. mkdir -p /workspaces/share -mkdir -p /workspaces/share/test /workspaces/share/tmpdir /workspaces/share/kernel /workspaces/share/tmp - +mkdir -p /workspaces/share/test /workspaces/share/tmp /workspaces/share/kernel /workspaces/share/tmpdir mountpoint -q /workspaces/share/test || mount --bind /home/v9fs-test/test /workspaces/share/test -mountpoint -q /workspaces/share/kernel || mount --bind /workspaces/kernel /workspaces/share/kernel mountpoint -q /workspaces/share/tmp || mount --bind /workspaces/tmp /workspaces/share/tmp +mountpoint -q /workspaces/share/kernel || mount --bind /workspaces/kernel /workspaces/share/kernel export FSDEV_PATH="/workspaces/share" export KERNELBUILD="${KERNELBUILD:-/workspaces/kernel/.build}" -# Build initrd each run (cheap; keeps guest runner in sync). -export INITRD="/home/v9fs-test/initrd.patched.cpio" -./scripts/v9fs-build-initrd "${INITRD}" +# Use the image-provided u-root/cpu initrd (starts cpud as PID 1). +export INITRD="${INITRD:-/home/v9fs-test/initrd.cpio}" -# Ask initrd to run guest tests, and propagate timestamp so the guest writes -# guest.exitcode into the same logs// directory the host checks. -export EXTRA_APPEND="v9fs.run=1 v9fs.tests=${tests} v9fs.ts=${TIMESTAMP}" +# Keep kernel cmdline for debugging, but don't rely on it for test execution. +export EXTRA_APPEND="v9fs.ts=${TIMESTAMP}" /home/v9fs-test/test/qemu.bash -# Wait for QEMU to exit (guest powers off). +echo "Waiting for guest cpud..." +for _ in $(seq 1 60); do + if "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 uname -rm >/dev/null 2>&1; then + break + fi + sleep 1 +done + +echo "Running guest tests via cpu (tests=${tests} type=${type} check=${check})" +export LOG="logs/${TIMESTAMP}" +"${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 \ + /mnt/9/workspaces/share/test/scripts/v9fs-guest-run "${tests}" "${type}" "${check}" || true + +# Wait for QEMU to exit (guest may still be running; kill if needed). if [ -f "${PIDFILE}" ]; then pid="$(cat "${PIDFILE}")" - while kill -0 "${pid}" 2>/dev/null; do + for _ in $(seq 1 10); do + kill -0 "${pid}" 2>/dev/null || break sleep 1 done + kill "${pid}" 2>/dev/null || true fi chmod -R a+rX "logs/${TIMESTAMP}" 2>/dev/null || true guest_rc_file="logs/${TIMESTAMP}/guest.exitcode" -for _ in 1 2 3 4 5; do - [ -f "${guest_rc_file}" ] && break - sleep 1 -done - if [ ! -f "${guest_rc_file}" ]; then echo "WARNING: missing ${guest_rc_file}; treating as failure" exit 2 From 21c7ca579de1c6bedda9500b1487a8b460663f4c Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:20:16 -0500 Subject: [PATCH 40/69] cpu runner: use localhost for hostfwd target QEMU usernet hostfwd binds to 127.0.0.1; connect via 'localhost' so cpu uses the expected address family. Made-with: Cursor --- scripts/v9fs-run-tests | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index cd905da..b1fe60e 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -37,7 +37,7 @@ export EXTRA_APPEND="v9fs.ts=${TIMESTAMP}" echo "Waiting for guest cpud..." for _ in $(seq 1 60); do - if "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 uname -rm >/dev/null 2>&1; then + if "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s localhost uname -rm >/dev/null 2>&1; then break fi sleep 1 @@ -45,7 +45,7 @@ done echo "Running guest tests via cpu (tests=${tests} type=${type} check=${check})" export LOG="logs/${TIMESTAMP}" -"${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 \ +"${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s localhost \ /mnt/9/workspaces/share/test/scripts/v9fs-guest-run "${tests}" "${type}" "${check}" || true # Wait for QEMU to exit (guest may still be running; kill if needed). From 562b0f69cbd853b17768fadf47c3fb9191cbbd1d Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:23:29 -0500 Subject: [PATCH 41/69] Pin v9fs/docker:2.0.0; stream QEMU serial into docker logs Use the tagged v9fs/docker 2.0.0 image everywhere. Run QEMU in the foreground with -serial stdio and tee output to logs//qemu.log so CI/local runs show progress directly in Docker logs. Made-with: Cursor --- .github/workflows/ci.yml | 2 +- .github/workflows/linux-kernel-publish.yml | 2 +- Makefile | 2 +- README.md | 25 +++++++------- UROOT.md | 2 +- qemu.bash | 38 +++++++++++++++++++++- scripts/v9fs-run-tests | 4 +++ 7 files changed, 57 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdfa261..d2a47e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-main' }} - V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:latest + V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:2.0.0 steps: - name: Checkout harness uses: actions/checkout@v4 diff --git a/.github/workflows/linux-kernel-publish.yml b/.github/workflows/linux-kernel-publish.yml index dcc6156..29f03a9 100644 --- a/.github/workflows/linux-kernel-publish.yml +++ b/.github/workflows/linux-kernel-publish.yml @@ -28,7 +28,7 @@ jobs: publish: runs-on: ubuntu-24.04-arm env: - V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:latest + V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:2.0.0 steps: - name: Checkout harness (scripts) uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index 826d660..3b4034d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: docker-smoke -IMAGE ?= ghcr.io/v9fs/docker:latest +IMAGE ?= ghcr.io/v9fs/docker:2.0.0 KERNEL_RELEASE ?= kernel-main docker-smoke: diff --git a/README.md b/README.md index e6c5438..520906c 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,14 @@ This repo now contains a **minimal 9p client smoke test harness**: ### Local smoke run (Docker + QEMU) -1) Build the image: +1. Build the image: ```bash -docker pull ghcr.io/v9fs/docker:latest +docker pull ghcr.io/v9fs/docker:2.0.0 ``` -2) Put a kernel `Image` at `./kernel/.build/arch/arm64/boot/Image` - -3) Run the smoke test: +1. Put a kernel `Image` at `./kernel/.build/arch/arm64/boot/Image` +2. Run the smoke test: ```bash make docker-smoke @@ -121,19 +120,20 @@ CI uses the same Docker + QEMU flow as local development, but **kernel builds ar published separately** from the harness tests: 1. A dedicated workflow builds `v9fs/linux` and publishes the arm64 `Image` to - **GitHub Releases** (and GHCR). + **GitHub Releases** (and GHCR). 2. The harness workflows download that published `Image` into `kernel/.build/arch/...` - and run `v9fs-run-tests ...` with `--privileged` so the harness can bind-mount a + and run `v9fs-run-tests ...` with `--privileged` so the harness can bind-mount a stable 9p export root (`/workspaces/share`). ### Workflows -| Workflow | File | When it runs | -| -------- | ---- | ------------ | -| **Publish Linux kernel** | `.github/workflows/linux-kernel-publish.yml` | **`repository_dispatch`** from `v9fs/linux` (recommended) or **`workflow_dispatch`** here. Builds arm64 `Image`, uploads it to a release tag you choose (`kernel-main`, `kernel-nightly`, `kernel-`, …), and pushes a GHCR bundle tagged by the **linux commit SHA** plus your release tag. | -| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), **manual** dispatch, or **`workflow_call`**. Downloads `Image` from the **`kernel-main`** release by default (override via `kernel_release`). | -| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule + **manual** dispatch. Downloads `Image` from **`kernel-nightly`** by default (override via `kernel_release`). | +| Workflow | File | When it runs | +| ------------------------ | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Publish Linux kernel** | `.github/workflows/linux-kernel-publish.yml` | `**repository_dispatch`** from `v9fs/linux` (recommended) or `**workflow_dispatch**` here. Builds arm64 `Image`, uploads it to a release tag you choose (`kernel-main`, `kernel-nightly`, `kernel-`, …), and pushes a GHCR bundle tagged by the **linux commit SHA** plus your release tag. | +| **CI (push and manual)** | `.github/workflows/demand.yml` | On **every push** (all branches), **manual** dispatch, or `**workflow_call`**. Downloads `Image` from the `**kernel-main**` release by default (override via `kernel_release`). | +| **Mainline** | `.github/workflows/nightly.yml` | **Daily** schedule + **manual** dispatch. Downloads `Image` from `**kernel-nightly`** by default (override via `kernel_release`). | + Because GitHub Actions cannot natively “watch” another repository’s pushes, the `v9fs/linux` repo should call `repository_dispatch` on `v9fs/test` when branches change @@ -161,7 +161,6 @@ The publish workflow uploads the **arm64** kernel `Image` as a stable release as - **Rolling nightly**: `https://github.com/v9fs/test/releases/download/kernel-nightly/Image` - **Versioned** (example): `https://github.com/v9fs/test/releases/download/kernel-6.12.0/Image` - Log artifact names (avoid collisions when jobs run in parallel): - CI manual/push: `test-results-ci`, `test-results-latency` diff --git a/UROOT.md b/UROOT.md index 72cb5c6..ebd776b 100644 --- a/UROOT.md +++ b/UROOT.md @@ -6,7 +6,7 @@ Per `AGENTS.md`, this is based on **implementation evidence**, not on secondary ## What the `v9fs/docker` initrd actually contains -From inspecting `/home/v9fs-test/initrd.cpio` inside the `ghcr.io/v9fs/docker:latest` image: +From inspecting `/home/v9fs-test/initrd.cpio` inside the `ghcr.io/v9fs/docker:2.0.0` image: - `init` is a **symlink** to `bbin/cpud` - `bbin/cpud`, `bbin/gosh`, and `bbin/init` are **symlinks** to `bb` diff --git a/qemu.bash b/qemu.bash index 6abd190..0a3617b 100755 --- a/qemu.bash +++ b/qemu.bash @@ -7,6 +7,8 @@ INITRD="${INITRD:-/home/v9fs-test/initrd.cpio}" LOG="${QEMULOG:-/home/v9fs-test/qemu.log}" PIDFILE="${PIDFILE:-/home/v9fs-test/qemu.pid}" FSDEV_PATH="${FSDEV_PATH:-/workspaces/share}" +QEMU_DAEMONIZE="${QEMU_DAEMONIZE:-0}" +QEMU_STREAM_LOG="${QEMU_STREAM_LOG:-1}" if test -f "${PIDFILE}"; then kill "$(cat "${PIDFILE}")" 2>/dev/null || true @@ -24,7 +26,41 @@ else exit 2 fi -exec ${QEMU} \ +qemu_args=( + -kernel "${KERNEL}" + -cpu "${QEMUCPU}" + -machine "${MACHINE}" + -smp 4 + -m 4096m + -initrd "${INITRD}" + -object rng-random,filename=/dev/urandom,id=rng0 + -device virtio-rng-pci,rng=rng0 + -device virtio-net-pci,netdev=n1 + -netdev user,id=n1,hostfwd=tcp:127.0.0.1:17010-:17010 + -nographic + -fsdev "local,security_model=none,writeout=immediate,id=fsdev0,path=${FSDEV_PATH}" + -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare + -append "${APPEND} ${EXTRA_APPEND:-}" +) + +if [ "${QEMU_DAEMONIZE}" = "1" ]; then + qemu_args+=( + -serial "file:${LOG}" + ${EXTRA} + -daemonize -display none -pidfile "${PIDFILE}" + ) + exec "${QEMU}" "${qemu_args[@]}" +fi + +# Foreground mode: stream serial to Docker logs and also tee to LOG. +if [ "${QEMU_STREAM_LOG}" = "1" ]; then + # Write pidfile ourselves in foreground mode. + echo "$$" >"${PIDFILE}" 2>/dev/null || true + "${QEMU}" "${qemu_args[@]}" ${EXTRA} -serial stdio 2>&1 | tee "${LOG}" +else + echo "$$" >"${PIDFILE}" 2>/dev/null || true + exec "${QEMU}" "${qemu_args[@]}" ${EXTRA} -serial stdio +fi -kernel "${KERNEL}" \ -cpu "${QEMUCPU}" \ -machine "${MACHINE}" \ diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index b1fe60e..0506caa 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -33,6 +33,10 @@ export INITRD="${INITRD:-/home/v9fs-test/initrd.cpio}" # Keep kernel cmdline for debugging, but don't rely on it for test execution. export EXTRA_APPEND="v9fs.ts=${TIMESTAMP}" +# Stream QEMU serial into the container logs (and tee to logs//qemu.log). +export QEMU_DAEMONIZE=0 +export QEMU_STREAM_LOG=1 + /home/v9fs-test/test/qemu.bash echo "Waiting for guest cpud..." From d1d6f9c47776349e80a5d167637b24e23aad6ffa Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:25:27 -0500 Subject: [PATCH 42/69] Revert docker image tag to latest Back out the temporary v9fs/docker:2.0.0 pin (tag not available yet). Keep the improved foreground QEMU logging. Made-with: Cursor --- .github/workflows/ci.yml | 2 +- .github/workflows/linux-kernel-publish.yml | 2 +- Makefile | 2 +- README.md | 2 +- UROOT.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2a47e6..bdfa261 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-main' }} - V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:2.0.0 + V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:latest steps: - name: Checkout harness uses: actions/checkout@v4 diff --git a/.github/workflows/linux-kernel-publish.yml b/.github/workflows/linux-kernel-publish.yml index 29f03a9..dcc6156 100644 --- a/.github/workflows/linux-kernel-publish.yml +++ b/.github/workflows/linux-kernel-publish.yml @@ -28,7 +28,7 @@ jobs: publish: runs-on: ubuntu-24.04-arm env: - V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:2.0.0 + V9FS_DOCKER_IMAGE: ghcr.io/v9fs/docker:latest steps: - name: Checkout harness (scripts) uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index 3b4034d..826d660 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: docker-smoke -IMAGE ?= ghcr.io/v9fs/docker:2.0.0 +IMAGE ?= ghcr.io/v9fs/docker:latest KERNEL_RELEASE ?= kernel-main docker-smoke: diff --git a/README.md b/README.md index 520906c..66566d7 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This repo now contains a **minimal 9p client smoke test harness**: 1. Build the image: ```bash -docker pull ghcr.io/v9fs/docker:2.0.0 +docker pull ghcr.io/v9fs/docker:latest ``` 1. Put a kernel `Image` at `./kernel/.build/arch/arm64/boot/Image` diff --git a/UROOT.md b/UROOT.md index ebd776b..72cb5c6 100644 --- a/UROOT.md +++ b/UROOT.md @@ -6,7 +6,7 @@ Per `AGENTS.md`, this is based on **implementation evidence**, not on secondary ## What the `v9fs/docker` initrd actually contains -From inspecting `/home/v9fs-test/initrd.cpio` inside the `ghcr.io/v9fs/docker:2.0.0` image: +From inspecting `/home/v9fs-test/initrd.cpio` inside the `ghcr.io/v9fs/docker:latest` image: - `init` is a **symlink** to `bbin/cpud` - `bbin/cpud`, `bbin/gosh`, and `bbin/init` are **symlinks** to `bb` From 430c77ad224dc1b37716acd6c22528957181e78b Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:26:28 -0500 Subject: [PATCH 43/69] QEMU foreground: use -nographic (no -serial stdio) Fix stdio contention: -nographic already consumes stdio, so foreground streaming now relies on -nographic alone and tees output to qemu.log. Made-with: Cursor --- qemu.bash | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qemu.bash b/qemu.bash index 0a3617b..e7a722f 100755 --- a/qemu.bash +++ b/qemu.bash @@ -37,7 +37,6 @@ qemu_args=( -device virtio-rng-pci,rng=rng0 -device virtio-net-pci,netdev=n1 -netdev user,id=n1,hostfwd=tcp:127.0.0.1:17010-:17010 - -nographic -fsdev "local,security_model=none,writeout=immediate,id=fsdev0,path=${FSDEV_PATH}" -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare -append "${APPEND} ${EXTRA_APPEND:-}" @@ -56,10 +55,11 @@ fi if [ "${QEMU_STREAM_LOG}" = "1" ]; then # Write pidfile ourselves in foreground mode. echo "$$" >"${PIDFILE}" 2>/dev/null || true - "${QEMU}" "${qemu_args[@]}" ${EXTRA} -serial stdio 2>&1 | tee "${LOG}" + # Use -nographic so QEMU routes serial+monitor to stdio once. + "${QEMU}" "${qemu_args[@]}" ${EXTRA} -nographic 2>&1 | tee "${LOG}" else echo "$$" >"${PIDFILE}" 2>/dev/null || true - exec "${QEMU}" "${qemu_args[@]}" ${EXTRA} -serial stdio + exec "${QEMU}" "${qemu_args[@]}" ${EXTRA} -nographic fi -kernel "${KERNEL}" \ -cpu "${QEMUCPU}" \ From b9f26ed63e19a09caaa15e8315932a1c7a9a4b95 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:26:55 -0500 Subject: [PATCH 44/69] Fix v9fs/docker paths: use /opt/v9fs initrd and cpu keys The current v9fs/docker:latest layout stores initrd and cpu tooling under /opt/v9fs; update runner defaults accordingly. Made-with: Cursor --- scripts/v9fs-run-tests | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 0506caa..dc0638c 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -9,8 +9,8 @@ check="${3:-1}" # GitHub Actions runner user when uploading `logs/` as an artifact. umask 022 -cpu_bin="${CPU_BIN:-/home/v9fs-test/go/bin/cpu}" -cpu_key="${CPU_KEY:-/home/v9fs-test/.ssh/identity}" +cpu_bin="${CPU_BIN:-/opt/v9fs/go/bin/cpu}" +cpu_key="${CPU_KEY:-/opt/v9fs/.ssh/identity}" export TIMESTAMP="${TIMESTAMP:-$(date +%s)}" mkdir -p "logs/${TIMESTAMP}" @@ -28,7 +28,7 @@ export FSDEV_PATH="/workspaces/share" export KERNELBUILD="${KERNELBUILD:-/workspaces/kernel/.build}" # Use the image-provided u-root/cpu initrd (starts cpud as PID 1). -export INITRD="${INITRD:-/home/v9fs-test/initrd.cpio}" +export INITRD="${INITRD:-/opt/v9fs/initrd.cpio}" # Keep kernel cmdline for debugging, but don't rely on it for test execution. export EXTRA_APPEND="v9fs.ts=${TIMESTAMP}" From 69623b223737f15436434b50cc7957d2a17f7306 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:28:09 -0500 Subject: [PATCH 45/69] cpu connect: avoid localhost/IPv6 mismatch; show last error Bind QEMU hostfwd on 0.0.0.0 and connect to 127.0.0.1 explicitly. During wait-for-cpud, capture and print the last cpu error so stalls are debuggable in docker logs. Made-with: Cursor --- qemu.bash | 2 +- scripts/v9fs-run-tests | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/qemu.bash b/qemu.bash index e7a722f..69d8a9e 100755 --- a/qemu.bash +++ b/qemu.bash @@ -36,7 +36,7 @@ qemu_args=( -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0 -device virtio-net-pci,netdev=n1 - -netdev user,id=n1,hostfwd=tcp:127.0.0.1:17010-:17010 + -netdev user,id=n1,hostfwd=tcp:0.0.0.0:17010-:17010 -fsdev "local,security_model=none,writeout=immediate,id=fsdev0,path=${FSDEV_PATH}" -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare -append "${APPEND} ${EXTRA_APPEND:-}" diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index dc0638c..7066756 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -40,16 +40,26 @@ export QEMU_STREAM_LOG=1 /home/v9fs-test/test/qemu.bash echo "Waiting for guest cpud..." +last_err="" for _ in $(seq 1 60); do - if "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s localhost uname -rm >/dev/null 2>&1; then + if out="$("${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 uname -rm 2>&1)"; then + echo "Guest cpud is up." break fi + last_err="$out" sleep 1 done +if ! "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 uname -rm >/dev/null 2>&1; then + echo "ERROR: cpud did not become reachable." + echo "Last cpu error:" + printf '%s\n' "${last_err}" + exit 2 +fi + echo "Running guest tests via cpu (tests=${tests} type=${type} check=${check})" export LOG="logs/${TIMESTAMP}" -"${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s localhost \ +"${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 \ /mnt/9/workspaces/share/test/scripts/v9fs-guest-run "${tests}" "${type}" "${check}" || true # Wait for QEMU to exit (guest may still be running; kill if needed). From a4e41a6e2076b0eed70d7ef6d94aea7aae15716e Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:29:06 -0500 Subject: [PATCH 46/69] Daemonize QEMU; tail qemu.log for live output Let v9fs-run-tests proceed to cpu connection by running QEMU in the background, while streaming guest output into docker logs via tail -F on logs//qemu.log. Made-with: Cursor --- scripts/v9fs-run-tests | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 7066756..505c501 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -33,12 +33,19 @@ export INITRD="${INITRD:-/opt/v9fs/initrd.cpio}" # Keep kernel cmdline for debugging, but don't rely on it for test execution. export EXTRA_APPEND="v9fs.ts=${TIMESTAMP}" -# Stream QEMU serial into the container logs (and tee to logs//qemu.log). -export QEMU_DAEMONIZE=0 -export QEMU_STREAM_LOG=1 +# Run QEMU daemonized, but stream qemu.log into docker logs. +export QEMU_DAEMONIZE=1 +export QEMU_STREAM_LOG=0 /home/v9fs-test/test/qemu.bash +# Stream guest serial log into the docker log while we proceed. +tail_pid="" +if [ -f "${QEMULOG}" ]; then + tail -n +1 -F "${QEMULOG}" & + tail_pid="$!" +fi + echo "Waiting for guest cpud..." last_err="" for _ in $(seq 1 60); do @@ -72,6 +79,10 @@ if [ -f "${PIDFILE}" ]; then kill "${pid}" 2>/dev/null || true fi +if [ -n "${tail_pid}" ]; then + kill "${tail_pid}" 2>/dev/null || true +fi + chmod -R a+rX "logs/${TIMESTAMP}" 2>/dev/null || true guest_rc_file="logs/${TIMESTAMP}/guest.exitcode" From e918988339a8e65a3e729c07ca1ea7a5b7dc1dcb Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:30:41 -0500 Subject: [PATCH 47/69] cpud wait: probe cpu connectivity with timeouts + errors During Wait for guest cpud, run a real cpu uname probe under timeout, increase timeout9p, and print periodic error output so we can see why the connection isn't succeeding. Made-with: Cursor --- scripts/v9fs-run-tests | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 505c501..9f9e1ef 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -48,16 +48,24 @@ fi echo "Waiting for guest cpud..." last_err="" -for _ in $(seq 1 60); do - if out="$("${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 uname -rm 2>&1)"; then +for i in $(seq 1 60); do + # Ensure a stuck connect doesn't hang the harness. + # `cpu` can block on 9p mount negotiation; cap total time per probe. + if out="$(timeout 5s "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 5s 127.0.0.1 uname -rm 2>&1)"; then echo "Guest cpud is up." + echo "cpu uname: ${out}" break fi last_err="$out" + if [ $((i % 5)) -eq 0 ]; then + echo "Still waiting for cpud... (attempt ${i}/60)" + echo "Last cpu error:" + printf '%s\n' "${last_err}" + fi sleep 1 done -if ! "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 uname -rm >/dev/null 2>&1; then +if ! timeout 5s "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 5s 127.0.0.1 uname -rm >/dev/null 2>&1; then echo "ERROR: cpud did not become reachable." echo "Last cpu error:" printf '%s\n' "${last_err}" From ad7a775db5ec359ca74f490c426dea8e5cbeaa9a Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:31:37 -0500 Subject: [PATCH 48/69] cpud wait: show exit code vs timeout; enable cpu debug Capture cpu probe exit status separately from output so we can distinguish timeouts from immediate failures, and run probes with -d to surface connection errors in docker logs. Made-with: Cursor --- scripts/v9fs-run-tests | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 9f9e1ef..299564b 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -51,12 +51,20 @@ last_err="" for i in $(seq 1 60); do # Ensure a stuck connect doesn't hang the harness. # `cpu` can block on 9p mount negotiation; cap total time per probe. - if out="$(timeout 5s "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 5s 127.0.0.1 uname -rm 2>&1)"; then + set +e + out="$(timeout 5s "${cpu_bin}" -d -key "${cpu_key}" -sp 17010 -timeout9p 5s 127.0.0.1 uname -rm 2>&1)" + st="$?" + set -e + if [ "${st}" = "0" ]; then echo "Guest cpud is up." echo "cpu uname: ${out}" break fi - last_err="$out" + if [ "${st}" = "124" ]; then + last_err="cpu probe timed out (5s)" + else + last_err="cpu probe failed (exit ${st}): ${out}" + fi if [ $((i % 5)) -eq 0 ]; then echo "Still waiting for cpud... (attempt ${i}/60)" echo "Last cpu error:" @@ -65,7 +73,7 @@ for i in $(seq 1 60); do sleep 1 done -if ! timeout 5s "${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 5s 127.0.0.1 uname -rm >/dev/null 2>&1; then +if ! timeout 5s "${cpu_bin}" -d -key "${cpu_key}" -sp 17010 -timeout9p 5s 127.0.0.1 uname -rm >/dev/null 2>&1; then echo "ERROR: cpud did not become reachable." echo "Last cpu error:" printf '%s\n' "${last_err}" From c435e0fa72f73a4f282b357327ada1fc0af9dc56 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:32:31 -0500 Subject: [PATCH 49/69] cpud wait: add tcp port probe; force cpu to tcp4 Check whether hostfwd port 17010 is accepting connections and force cpu client to use tcp4 to avoid any dual-stack ambiguity. Also extend probe timeouts slightly. Made-with: Cursor --- scripts/v9fs-run-tests | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 299564b..db5e45a 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -49,10 +49,18 @@ fi echo "Waiting for guest cpud..." last_err="" for i in $(seq 1 60); do + set +e + timeout 1s bash -lc '/dev/null 2>&1 + tcp_st="$?" + set -e + if [ "${tcp_st}" != "0" ] && [ $((i % 5)) -eq 0 ]; then + echo "Port 17010 not accepting connections yet (attempt ${i}/60)" + fi + # Ensure a stuck connect doesn't hang the harness. # `cpu` can block on 9p mount negotiation; cap total time per probe. set +e - out="$(timeout 5s "${cpu_bin}" -d -key "${cpu_key}" -sp 17010 -timeout9p 5s 127.0.0.1 uname -rm 2>&1)" + out="$(timeout 8s "${cpu_bin}" -d -net tcp4 -key "${cpu_key}" -sp 17010 -timeout9p 8s 127.0.0.1 uname -rm 2>&1)" st="$?" set -e if [ "${st}" = "0" ]; then From 13c6e50049d9faaadcf0974b6a04d8845f12e89a Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:38:48 -0500 Subject: [PATCH 50/69] cpud wait: log when port 17010 becomes reachable Print a one-time message when the forwarded cpud port starts accepting connections, to distinguish port-forward issues from cpu handshake hangs. Made-with: Cursor --- scripts/v9fs-run-tests | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index db5e45a..db68539 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -48,11 +48,16 @@ fi echo "Waiting for guest cpud..." last_err="" +port_ok=0 for i in $(seq 1 60); do set +e timeout 1s bash -lc '/dev/null 2>&1 tcp_st="$?" set -e + if [ "${tcp_st}" = "0" ] && [ "${port_ok}" = "0" ]; then + port_ok=1 + echo "Port 17010 is accepting connections." + fi if [ "${tcp_st}" != "0" ] && [ $((i % 5)) -eq 0 ]; then echo "Port 17010 not accepting connections yet (attempt ${i}/60)" fi From 0006361844cad87374386f717f537d9b2672b310 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:39:43 -0500 Subject: [PATCH 51/69] Use cpu -ssh for liveness + test execution The u-root cpu client can hang negotiating its internal 9p mount; use -ssh (and tcp4) for the cpud liveness probe and to run v9fs-guest-run since the guest already has the repo via QEMU 9p. Made-with: Cursor --- scripts/v9fs-run-tests | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index db68539..c756aea 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -65,7 +65,8 @@ for i in $(seq 1 60); do # Ensure a stuck connect doesn't hang the harness. # `cpu` can block on 9p mount negotiation; cap total time per probe. set +e - out="$(timeout 8s "${cpu_bin}" -d -net tcp4 -key "${cpu_key}" -sp 17010 -timeout9p 8s 127.0.0.1 uname -rm 2>&1)" + # For a basic liveness probe, use SSH-only mode to avoid hanging on 9p mount negotiation. + out="$(timeout 8s "${cpu_bin}" -d -ssh -net tcp4 -key "${cpu_key}" -sp 17010 127.0.0.1 uname -rm 2>&1)" st="$?" set -e if [ "${st}" = "0" ]; then @@ -86,7 +87,7 @@ for i in $(seq 1 60); do sleep 1 done -if ! timeout 5s "${cpu_bin}" -d -key "${cpu_key}" -sp 17010 -timeout9p 5s 127.0.0.1 uname -rm >/dev/null 2>&1; then +if ! timeout 5s "${cpu_bin}" -d -ssh -net tcp4 -key "${cpu_key}" -sp 17010 127.0.0.1 uname -rm >/dev/null 2>&1; then echo "ERROR: cpud did not become reachable." echo "Last cpu error:" printf '%s\n' "${last_err}" @@ -95,7 +96,7 @@ fi echo "Running guest tests via cpu (tests=${tests} type=${type} check=${check})" export LOG="logs/${TIMESTAMP}" -"${cpu_bin}" -key "${cpu_key}" -sp 17010 -timeout9p 2s 127.0.0.1 \ +"${cpu_bin}" -ssh -net tcp4 -key "${cpu_key}" -sp 17010 127.0.0.1 \ /mnt/9/workspaces/share/test/scripts/v9fs-guest-run "${tests}" "${type}" "${check}" || true # Wait for QEMU to exit (guest may still be running; kill if needed). From 79affa84f0586cbac5a036a3e7b331349ccce962 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:43:44 -0500 Subject: [PATCH 52/69] Smoke: guest-direct mount+ls over 9p (no cpu) Patch the base v9fs/docker initrd to run a /bin/uinit that mounts the QEMU 9p export and lists it, writes guest.exitcode, then powers off. v9fs-run-tests now validates this marker instead of waiting for cpud/cpu. Made-with: Cursor --- qemu.bash | 17 ---------- scripts/v9fs-build-initrd | 32 ++++++++---------- scripts/v9fs-run-tests | 71 ++++----------------------------------- 3 files changed, 21 insertions(+), 99 deletions(-) diff --git a/qemu.bash b/qemu.bash index 69d8a9e..1eeb5db 100755 --- a/qemu.bash +++ b/qemu.bash @@ -61,20 +61,3 @@ else echo "$$" >"${PIDFILE}" 2>/dev/null || true exec "${QEMU}" "${qemu_args[@]}" ${EXTRA} -nographic fi - -kernel "${KERNEL}" \ - -cpu "${QEMUCPU}" \ - -machine "${MACHINE}" \ - -smp 4 \ - -m 4096m \ - -initrd "${INITRD}" \ - -object rng-random,filename=/dev/urandom,id=rng0 \ - -device virtio-rng-pci,rng=rng0 \ - -device virtio-net-pci,netdev=n1 \ - -netdev user,id=n1,hostfwd=tcp:127.0.0.1:17010-:17010 \ - -serial "file:${LOG}" \ - -fsdev "local,security_model=none,writeout=immediate,id=fsdev0,path=${FSDEV_PATH}" \ - -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare \ - -append "${APPEND} ${EXTRA_APPEND:-}" \ - ${EXTRA} \ - -daemonize -display none -pidfile "${PIDFILE}" - diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index adba66a..2b99e99 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -1,12 +1,12 @@ #!/usr/bin/env bash set -euo pipefail -out="${1:-/home/v9fs-test/initrd.patched.cpio}" +out="${1:-/opt/v9fs/initrd.patched.cpio}" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT # We run inside the `v9fs/docker` image, which provides a base initrd at this path. -base="${UROOT_BASE:-/home/v9fs-test/initrd.cpio}" +base="${UROOT_BASE:-/opt/v9fs/initrd.cpio}" if [ ! -f "$base" ]; then echo "ERROR: base initrd not found at $base" exit 2 @@ -31,28 +31,18 @@ say() { } cmdline="$(/dev/null || true)" -run=0 -tests="smoke" ts="" for w in $cmdline; do case "$w" in - v9fs.run=1) run=1 ;; - v9fs.tests=*) tests="${w#v9fs.tests=}" ;; v9fs.ts=*) ts="${w#v9fs.ts=}" ;; esac done -if [ "$run" != "1" ]; then - say "uinit: v9fs.run not set; powering off" - "$bb" poweroff -f || "$bb" halt -f || "$bb" reboot -f - exit 0 -fi - if [ -n "${ts:-}" ]; then export TIMESTAMP="$ts" fi -say "uinit: starting tests=$tests ts=${TIMESTAMP:-unset}" +say "uinit: mount+ls smoke ts=${TIMESTAMP:-unset}" if ! "$bb" mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then say "uinit: ERROR: failed to mount hostshare on /mnt/9" @@ -61,11 +51,17 @@ if ! "$bb" mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /m exit 1 fi -if [ -x /mnt/9/test/scripts/v9fs-guest-run ]; then - /bin/sh /mnt/9/test/scripts/v9fs-guest-run "$tests" || true -else - say "uinit: missing /mnt/9/test/scripts/v9fs-guest-run" - "$bb" ls -la /mnt/9/test/scripts 2>/dev/null || true +say "uinit: listing /mnt/9" +"$bb" ls -la /mnt/9 || true +"$bb" ls -la /mnt/9/workspaces 2>/dev/null || true +"$bb" ls -la /mnt/9/workspaces/share 2>/dev/null || true + +# Write a pass/fail marker into the host-visible logs directory. +repo="/mnt/9/workspaces/share/test" +if [ -n "${TIMESTAMP:-}" ] && [ -d "${repo}" ]; then + "$bb" mkdir -p "${repo}/logs/${TIMESTAMP}" || true + echo 0 >"${repo}/logs/${TIMESTAMP}/guest.exitcode" 2>/dev/null || true + echo "uinit: OK" >"${repo}/logs/${TIMESTAMP}/guest.log" 2>/dev/null || true fi "$bb" sync 2>/dev/null || true diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index c756aea..364f202 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -2,16 +2,11 @@ set -euo pipefail tests="${1:-smoke}" -type="${2:-ci}" -check="${3:-1}" # Ensure artifacts created by root inside Docker are readable by the # GitHub Actions runner user when uploading `logs/` as an artifact. umask 022 -cpu_bin="${CPU_BIN:-/opt/v9fs/go/bin/cpu}" -cpu_key="${CPU_KEY:-/opt/v9fs/.ssh/identity}" - export TIMESTAMP="${TIMESTAMP:-$(date +%s)}" mkdir -p "logs/${TIMESTAMP}" export QEMULOG="logs/${TIMESTAMP}/qemu.log" @@ -27,10 +22,11 @@ mountpoint -q /workspaces/share/kernel || mount --bind /workspaces/kernel /works export FSDEV_PATH="/workspaces/share" export KERNELBUILD="${KERNELBUILD:-/workspaces/kernel/.build}" -# Use the image-provided u-root/cpu initrd (starts cpud as PID 1). -export INITRD="${INITRD:-/opt/v9fs/initrd.cpio}" +# Patch the image-provided initrd so u-root runs our `/bin/uinit`. +export INITRD="${INITRD:-/opt/v9fs/initrd.patched.cpio}" +./scripts/v9fs-build-initrd "${INITRD}" -# Keep kernel cmdline for debugging, but don't rely on it for test execution. +# Pass timestamp so uinit writes guest.exitcode into logs//. export EXTRA_APPEND="v9fs.ts=${TIMESTAMP}" # Run QEMU daemonized, but stream qemu.log into docker logs. @@ -46,67 +42,14 @@ if [ -f "${QEMULOG}" ]; then tail_pid="$!" fi -echo "Waiting for guest cpud..." -last_err="" -port_ok=0 -for i in $(seq 1 60); do - set +e - timeout 1s bash -lc '/dev/null 2>&1 - tcp_st="$?" - set -e - if [ "${tcp_st}" = "0" ] && [ "${port_ok}" = "0" ]; then - port_ok=1 - echo "Port 17010 is accepting connections." - fi - if [ "${tcp_st}" != "0" ] && [ $((i % 5)) -eq 0 ]; then - echo "Port 17010 not accepting connections yet (attempt ${i}/60)" - fi - - # Ensure a stuck connect doesn't hang the harness. - # `cpu` can block on 9p mount negotiation; cap total time per probe. - set +e - # For a basic liveness probe, use SSH-only mode to avoid hanging on 9p mount negotiation. - out="$(timeout 8s "${cpu_bin}" -d -ssh -net tcp4 -key "${cpu_key}" -sp 17010 127.0.0.1 uname -rm 2>&1)" - st="$?" - set -e - if [ "${st}" = "0" ]; then - echo "Guest cpud is up." - echo "cpu uname: ${out}" - break - fi - if [ "${st}" = "124" ]; then - last_err="cpu probe timed out (5s)" - else - last_err="cpu probe failed (exit ${st}): ${out}" - fi - if [ $((i % 5)) -eq 0 ]; then - echo "Still waiting for cpud... (attempt ${i}/60)" - echo "Last cpu error:" - printf '%s\n' "${last_err}" - fi - sleep 1 -done - -if ! timeout 5s "${cpu_bin}" -d -ssh -net tcp4 -key "${cpu_key}" -sp 17010 127.0.0.1 uname -rm >/dev/null 2>&1; then - echo "ERROR: cpud did not become reachable." - echo "Last cpu error:" - printf '%s\n' "${last_err}" - exit 2 -fi - -echo "Running guest tests via cpu (tests=${tests} type=${type} check=${check})" -export LOG="logs/${TIMESTAMP}" -"${cpu_bin}" -ssh -net tcp4 -key "${cpu_key}" -sp 17010 127.0.0.1 \ - /mnt/9/workspaces/share/test/scripts/v9fs-guest-run "${tests}" "${type}" "${check}" || true - -# Wait for QEMU to exit (guest may still be running; kill if needed). +# Wait for QEMU to exit (guest should power off). if [ -f "${PIDFILE}" ]; then pid="$(cat "${PIDFILE}")" - for _ in $(seq 1 10); do + for _ in $(seq 1 120); do kill -0 "${pid}" 2>/dev/null || break sleep 1 done - kill "${pid}" 2>/dev/null || true + kill -0 "${pid}" 2>/dev/null && echo "WARNING: QEMU still running; killing" && kill "${pid}" 2>/dev/null || true fi if [ -n "${tail_pid}" ]; then From ab7f6a4816bc5a89476e3243eb048f6dadf4afa8 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:45:07 -0500 Subject: [PATCH 53/69] uinit: idle if poweroff fails (avoid init exit panic) When u-root init runs /bin/uinit, it will panic if PID1 exits. Ensure uinit loops forever if poweroff/halt/reboot returns unexpectedly. Made-with: Cursor --- scripts/v9fs-build-initrd | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 2b99e99..bc0a02f 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -65,7 +65,15 @@ if [ -n "${TIMESTAMP:-}" ] && [ -d "${repo}" ]; then fi "$bb" sync 2>/dev/null || true -"$bb" poweroff -f || "$bb" halt -f || "$bb" reboot -f + +say "uinit: powering off" +"$bb" poweroff -f || "$bb" halt -f || "$bb" reboot -f || true + +# If poweroff fails for any reason, do not let PID1 exit (kernel will panic). +say "uinit: poweroff returned; idling" +while true; do + "$bb" sleep 1 || true +done EOF chmod +x "$uinit" From eb00e3621a7bea31c6c772c84019e6101905e571 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:47:29 -0500 Subject: [PATCH 54/69] Initrd: inject mount/ls/sh and use custom /init for 9p ls Build a patched initrd that replaces /init with a small script, and injects /bin/sh,/bin/mount,/bin/ls plus their shared libraries from the container so the guest can mount the 9p export and ls it reliably. Made-with: Cursor --- scripts/v9fs-build-initrd | 149 +++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 59 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index bc0a02f..a6efa5c 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -12,77 +12,98 @@ if [ ! -f "$base" ]; then exit 2 fi -uinit="$work/uinit" -cat >"$uinit" <<'EOF' +# Build a tiny /init script that uses traditional userspace binaries we copy in +# (mount/ls/sh), not u-root applets (which may not include mount/ls in this image). +init="$work/init" +cat >"$init" <<'EOF' #!/bin/sh set -eu - -# Force output to the serial console that QEMU captures. exec >/dev/console 2>&1 -mkdir -p /run /tmp /mnt/9 - -bb="/bbin/bb" +mount -t proc proc /proc || true +mount -t sysfs sysfs /sys || true +mount -t devtmpfs devtmpfs /dev || true -say() { - # best-effort: also mirror to kernel log if console is weird - echo "$@" || true - [ -c /dev/kmsg ] && echo "$@" >/dev/kmsg 2>/dev/null || true -} +mkdir -p /run /tmp /mnt/9 -cmdline="$(/dev/null || true)" ts="" -for w in $cmdline; do +for w in $(cat /proc/cmdline 2>/dev/null || true); do case "$w" in v9fs.ts=*) ts="${w#v9fs.ts=}" ;; esac done -if [ -n "${ts:-}" ]; then - export TIMESTAMP="$ts" -fi - -say "uinit: mount+ls smoke ts=${TIMESTAMP:-unset}" +echo "init: mount+ls 9p smoke ts=${ts:-unset}" -if ! "$bb" mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then - say "uinit: ERROR: failed to mount hostshare on /mnt/9" - "$bb" sync 2>/dev/null || true - "$bb" poweroff -f || "$bb" halt -f || "$bb" reboot -f - exit 1 +if mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then + echo "init: mounted hostshare at /mnt/9" + ls -la /mnt/9 || true +else + echo "init: ERROR: mount hostshare failed" fi -say "uinit: listing /mnt/9" -"$bb" ls -la /mnt/9 || true -"$bb" ls -la /mnt/9/workspaces 2>/dev/null || true -"$bb" ls -la /mnt/9/workspaces/share 2>/dev/null || true - -# Write a pass/fail marker into the host-visible logs directory. repo="/mnt/9/workspaces/share/test" -if [ -n "${TIMESTAMP:-}" ] && [ -d "${repo}" ]; then - "$bb" mkdir -p "${repo}/logs/${TIMESTAMP}" || true - echo 0 >"${repo}/logs/${TIMESTAMP}/guest.exitcode" 2>/dev/null || true - echo "uinit: OK" >"${repo}/logs/${TIMESTAMP}/guest.log" 2>/dev/null || true +if [ -n "${ts:-}" ] && [ -d "$repo" ]; then + mkdir -p "$repo/logs/$ts" || true + echo 0 >"$repo/logs/$ts/guest.exitcode" 2>/dev/null || true + echo "init: OK" >"$repo/logs/$ts/guest.log" 2>/dev/null || true fi -"$bb" sync 2>/dev/null || true - -say "uinit: powering off" -"$bb" poweroff -f || "$bb" halt -f || "$bb" reboot -f || true +sync 2>/dev/null || true +poweroff -f || halt -f || reboot -f || true -# If poweroff fails for any reason, do not let PID1 exit (kernel will panic). -say "uinit: poweroff returned; idling" +# Never exit PID1; if poweroff fails, just idle. +echo "init: poweroff returned; idling" while true; do - "$bb" sleep 1 || true + sleep 1 || true done EOF -chmod +x "$uinit" +chmod +x "$init" + +# Collect required userland binaries and their shared libs from the container FS. +mkdir -p "$work/rootfs" +copy_file() { + local src="$1" dst="$2" + mkdir -p "$work/rootfs/$(dirname "$dst")" + cp -L "$src" "$work/rootfs/$dst" +} +copy_libs() { + local bin="$1" + ldd "$bin" | awk '/=> \\/|^\\//{print $(NF-1)}' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | while read -r lib; do + [ -n "$lib" ] || continue + [ -f "$lib" ] || continue + copy_file "$lib" "$lib" + done +} + +shbin="$(command -v sh || true)" +mountbin="$(command -v mount || true)" +lsbin="$(command -v ls || true)" + +[ -n "$shbin" ] || { echo "ERROR: no sh in image"; exit 2; } +[ -n "$mountbin" ] || { echo "ERROR: no mount in image"; exit 2; } +[ -n "$lsbin" ] || { echo "ERROR: no ls in image"; exit 2; } + +copy_file "$shbin" "/bin/sh" +copy_file "$mountbin" "/bin/mount" +copy_file "$lsbin" "/bin/ls" +copy_libs "$shbin" +copy_libs "$mountbin" +copy_libs "$lsbin" + +# Also ensure dynamic loader exists if not captured above. +for ldso in /lib/ld-linux-aarch64.so.1 /lib64/ld-linux-aarch64.so.1; do + if [ -f "$ldso" ]; then + copy_file "$ldso" "$ldso" + fi +done # Overlay our /init onto the base initrd using a tiny Python newc (cpio) rewriter. # This avoids needing `cpio`, `busybox`, or a working u-root build toolchain. -python3 - "$base" "$uinit" "$out" <<'PY' +python3 - "$base" "$init" "$work/rootfs" "$out" <<'PY' import os, sys, stat, time -base_path, init_path, out_path = sys.argv[1:4] +base_path, init_path, extra_root, out_path = sys.argv[1:5] def align4(x: int) -> int: return (x + 3) & ~3 @@ -202,10 +223,22 @@ def write_symlink(w, name: str, target: str, uid: int = 0, gid: int = 0): def main(): init_data = open(init_path, "rb").read() - uinit_mode = stat.S_IFREG | 0o755 - - replaced = False - uinit_written = False + init_mode = stat.S_IFREG | 0o755 + + extras = {} # path -> (mode, data) + for root, _, files in os.walk(extra_root): + for fn in files: + src = os.path.join(root, fn) + rel = os.path.relpath(src, extra_root) + rel = rel.lstrip("./") + st = os.lstat(src) + if stat.S_ISLNK(st.st_mode): + tgt = os.readlink(src).encode("utf-8") + extras[rel] = (stat.S_IFLNK | 0o777, tgt) + elif stat.S_ISREG(st.st_mode): + extras[rel] = (stat.S_IFREG | (st.st_mode & 0o777), open(src, "rb").read()) + + replaced_init = False tmp_out = out_path + ".tmp" with open(base_path, "rb") as rf, open(tmp_out, "wb") as wf: for ent in iter_newc_entries(rf): @@ -213,21 +246,19 @@ def main(): if name == "TRAILER!!!": break if name in ("init", "/init"): - # Replace base init symlink (cpud) with u-root init. - write_symlink(wf, "init", "bbin/init") - replaced = True - elif name in ("bin/uinit", "/bin/uinit"): - write_entry(wf, "bin/uinit", uinit_mode, 0, 0, init_data) - uinit_written = True + write_entry(wf, "init", init_mode, 0, 0, init_data) + replaced_init = True else: write_entry(wf, name, ent["mode"], ent["uid"], ent["gid"], ent["data"]) - if not replaced: - # If base initrd didn't have init, add it before trailer. - write_symlink(wf, "init", "bbin/init") + if not replaced_init: + write_entry(wf, "init", init_mode, 0, 0, init_data) - if not uinit_written: - write_entry(wf, "bin/uinit", uinit_mode, 0, 0, init_data) + # Add extra binaries/libs (and overwrite if absent in base). + for path, (mode, data) in extras.items(): + # avoid double-writing files already present in base by only adding + # ones that are not in a minimal safe set; base initrd is tiny anyway. + write_entry(wf, path, mode, 0, 0, data) # trailer write_entry(wf, "TRAILER!!!", stat.S_IFREG | 0o644, 0, 0, b"") From 911c3cbc8b6ce81af159df22a2a8fde41dac246e Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:47:47 -0500 Subject: [PATCH 55/69] Fix ldd parsing in initrd builder Use a simpler awk expression to extract shared library paths from ldd output. Made-with: Cursor --- scripts/v9fs-build-initrd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index a6efa5c..836baf9 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -69,7 +69,7 @@ copy_file() { } copy_libs() { local bin="$1" - ldd "$bin" | awk '/=> \\/|^\\//{print $(NF-1)}' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | while read -r lib; do + ldd "$bin" | awk '/=> \\//{print $3} /^\\//{print $1}' | while read -r lib; do [ -n "$lib" ] || continue [ -f "$lib" ] || continue copy_file "$lib" "$lib" From 93c018c2e265703e8d32ec751161abdc8e2d67d3 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:48:03 -0500 Subject: [PATCH 56/69] Fix awk script separator in ldd parsing Add required semicolon between awk pattern actions for portability. Made-with: Cursor --- scripts/v9fs-build-initrd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 836baf9..6b9580d 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -69,7 +69,7 @@ copy_file() { } copy_libs() { local bin="$1" - ldd "$bin" | awk '/=> \\//{print $3} /^\\//{print $1}' | while read -r lib; do + ldd "$bin" | awk '/=> \\//{print $3}; /^\\//{print $1}' | while read -r lib; do [ -n "$lib" ] || continue [ -f "$lib" ] || continue copy_file "$lib" "$lib" From ac84810bfa5c8ec08ca4af8b762ca197065b0ee3 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:48:47 -0500 Subject: [PATCH 57/69] Fix awk regex for ldd library extraction Use portable awk patterns to capture absolute-path libraries from ldd output. Made-with: Cursor --- scripts/v9fs-build-initrd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 6b9580d..3db7131 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -69,7 +69,7 @@ copy_file() { } copy_libs() { local bin="$1" - ldd "$bin" | awk '/=> \\//{print $3}; /^\\//{print $1}' | while read -r lib; do + ldd "$bin" | awk '/=> \//{print $3}; /^\//{print $1}' | while read -r lib; do [ -n "$lib" ] || continue [ -f "$lib" ] || continue copy_file "$lib" "$lib" From 5518b62c5207c6bbb10081fdb0e2820bc7dcf097 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:50:29 -0500 Subject: [PATCH 58/69] Initrd rewrite: skip base entries we replace Avoid duplicate cpio paths (e.g. /bin/sh) by skipping base entries for any injected file so the kernel definitely sees our replaced binaries and init script. Made-with: Cursor --- scripts/v9fs-build-initrd | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 3db7131..b27af53 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -239,25 +239,27 @@ def main(): extras[rel] = (stat.S_IFREG | (st.st_mode & 0o777), open(src, "rb").read()) replaced_init = False + replace_names = set(extras.keys()) + replace_names.add("init") + tmp_out = out_path + ".tmp" with open(base_path, "rb") as rf, open(tmp_out, "wb") as wf: for ent in iter_newc_entries(rf): name = ent["name"] if name == "TRAILER!!!": break - if name in ("init", "/init"): - write_entry(wf, "init", init_mode, 0, 0, init_data) - replaced_init = True - else: - write_entry(wf, name, ent["mode"], ent["uid"], ent["gid"], ent["data"]) + # If we are replacing this path, skip the base entry so we don't + # rely on the kernel's behavior for duplicate cpio entries. + norm = name.lstrip("/") + if norm in replace_names: + continue + write_entry(wf, name, ent["mode"], ent["uid"], ent["gid"], ent["data"]) if not replaced_init: write_entry(wf, "init", init_mode, 0, 0, init_data) # Add extra binaries/libs (and overwrite if absent in base). for path, (mode, data) in extras.items(): - # avoid double-writing files already present in base by only adding - # ones that are not in a minimal safe set; base initrd is tiny anyway. write_entry(wf, path, mode, 0, 0, data) # trailer From 326811ebb8c14538e68c1c296bd7fdcbc657f441 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:51:43 -0500 Subject: [PATCH 59/69] Initrd: add directory entries for injected libs Write parent directory entries (e.g. /lib and /lib/aarch64-linux-gnu) before injecting shared libraries so the kernel can find the dynamic loader and libc. Made-with: Cursor --- scripts/v9fs-build-initrd | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index b27af53..ec51c98 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -221,6 +221,9 @@ def write_symlink(w, name: str, target: str, uid: int = 0, gid: int = 0): mode = stat.S_IFLNK | 0o777 write_entry(w, name, mode, uid, gid, target.encode("utf-8")) +def write_dir(w, name: str, mode: int = 0o755, uid: int = 0, gid: int = 0): + write_entry(w, name, stat.S_IFDIR | mode, uid, gid, b"") + def main(): init_data = open(init_path, "rb").read() init_mode = stat.S_IFREG | 0o755 @@ -259,6 +262,17 @@ def main(): write_entry(wf, "init", init_mode, 0, 0, init_data) # Add extra binaries/libs (and overwrite if absent in base). + # Ensure parent directories exist in the archive. + dirs = set() + for p in extras.keys(): + parts = p.split("/") + for i in range(1, len(parts)): + d = "/".join(parts[:i]) + if d: + dirs.add(d) + for d in sorted(dirs, key=lambda s: (s.count("/"), s)): + write_dir(wf, d) + for path, (mode, data) in extras.items(): write_entry(wf, path, mode, 0, 0, data) From d239b7e7da9862d9e63cddbeaf85757676054696 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:52:32 -0500 Subject: [PATCH 60/69] Initrd: include core utils needed by /init Inject mkdir/sync/sleep/poweroff/halt/reboot in addition to sh/mount/ls so the guest /init script can run without relying on u-root applets. Made-with: Cursor --- scripts/v9fs-build-initrd | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index ec51c98..bd87e12 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -76,20 +76,15 @@ copy_libs() { done } -shbin="$(command -v sh || true)" -mountbin="$(command -v mount || true)" -lsbin="$(command -v ls || true)" - -[ -n "$shbin" ] || { echo "ERROR: no sh in image"; exit 2; } -[ -n "$mountbin" ] || { echo "ERROR: no mount in image"; exit 2; } -[ -n "$lsbin" ] || { echo "ERROR: no ls in image"; exit 2; } - -copy_file "$shbin" "/bin/sh" -copy_file "$mountbin" "/bin/mount" -copy_file "$lsbin" "/bin/ls" -copy_libs "$shbin" -copy_libs "$mountbin" -copy_libs "$lsbin" +need_bins=( + sh mount ls mkdir sync sleep poweroff halt reboot +) +for b in "${need_bins[@]}"; do + p="$(command -v "$b" || true)" + [ -n "$p" ] || { echo "ERROR: missing $b in image"; exit 2; } + copy_file "$p" "/bin/$b" + copy_libs "$p" +done # Also ensure dynamic loader exists if not captured above. for ldso in /lib/ld-linux-aarch64.so.1 /lib64/ld-linux-aarch64.so.1; do From 5ed72ecdd019fa3c77837227df568a7d2e6a0234 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:52:54 -0500 Subject: [PATCH 61/69] Initrd: use sysrq for poweroff; don't require poweroff binary The v9fs/docker image doesn't ship poweroff/halt/reboot binaries. Use /proc/sysrq-trigger after sync to request shutdown, and only inject the minimal utils we need. Made-with: Cursor --- scripts/v9fs-build-initrd | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index bd87e12..12d5b01 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -50,7 +50,9 @@ if [ -n "${ts:-}" ] && [ -d "$repo" ]; then fi sync 2>/dev/null || true -poweroff -f || halt -f || reboot -f || true +echo "init: requesting poweroff via sysrq" +echo o >/proc/sysrq-trigger 2>/dev/null || true +echo b >/proc/sysrq-trigger 2>/dev/null || true # Never exit PID1; if poweroff fails, just idle. echo "init: poweroff returned; idling" @@ -77,7 +79,7 @@ copy_libs() { } need_bins=( - sh mount ls mkdir sync sleep poweroff halt reboot + sh mount ls mkdir sync sleep ) for b in "${need_bins[@]}"; do p="$(command -v "$b" || true)" From 9f69c746e8667d806c62dab48f5888ae33e85424 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:53:54 -0500 Subject: [PATCH 62/69] Guest /init: create /proc,/sys,/dev before mounting Without the directories, proc mount failed and v9fs.ts couldn't be read from /proc/cmdline, so guest.exitcode wasn't written. Create mountpoints first. Made-with: Cursor --- scripts/v9fs-build-initrd | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 12d5b01..4192a8d 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -20,9 +20,10 @@ cat >"$init" <<'EOF' set -eu exec >/dev/console 2>&1 -mount -t proc proc /proc || true -mount -t sysfs sysfs /sys || true -mount -t devtmpfs devtmpfs /dev || true +mkdir -p /proc /sys /dev +mount -t proc proc /proc 2>/dev/null || true +mount -t sysfs sysfs /sys 2>/dev/null || true +mount -t devtmpfs devtmpfs /dev 2>/dev/null || true mkdir -p /run /tmp /mnt/9 From bc6d689da9fbee7b0caa722aeef831c9350c98f8 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:54:41 -0500 Subject: [PATCH 63/69] Guest /init: don't redirect to /dev/null before devtmpfs Redirecting mount/sync/sysrq errors to /dev/null fails when /dev/null doesn't exist yet, preventing proc mount and cmdline parsing. Keep output on console until /dev is ready. Made-with: Cursor --- scripts/v9fs-build-initrd | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 4192a8d..2e96b2a 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -21,9 +21,9 @@ set -eu exec >/dev/console 2>&1 mkdir -p /proc /sys /dev -mount -t proc proc /proc 2>/dev/null || true -mount -t sysfs sysfs /sys 2>/dev/null || true -mount -t devtmpfs devtmpfs /dev 2>/dev/null || true +mount -t proc proc /proc || true +mount -t sysfs sysfs /sys || true +mount -t devtmpfs devtmpfs /dev || true mkdir -p /run /tmp /mnt/9 @@ -50,10 +50,10 @@ if [ -n "${ts:-}" ] && [ -d "$repo" ]; then echo "init: OK" >"$repo/logs/$ts/guest.log" 2>/dev/null || true fi -sync 2>/dev/null || true +sync || true echo "init: requesting poweroff via sysrq" -echo o >/proc/sysrq-trigger 2>/dev/null || true -echo b >/proc/sysrq-trigger 2>/dev/null || true +echo o >/proc/sysrq-trigger || true +echo b >/proc/sysrq-trigger || true # Never exit PID1; if poweroff fails, just idle. echo "init: poweroff returned; idling" From 72a274e8206e77a923c6d567b05e5ff63ca5293d Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:55:47 -0500 Subject: [PATCH 64/69] Smoke: write guest.exitcode to stable path Avoid relying on reading v9fs.ts from /proc/cmdline by writing guest.exitcode/guest.log to a stable host-visible path (logs/guest.*). v9fs-run-tests copies those into logs// for artifacts. Made-with: Cursor --- scripts/v9fs-build-initrd | 9 ++++----- scripts/v9fs-run-tests | 12 +++++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 2e96b2a..78ae462 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -44,16 +44,15 @@ else fi repo="/mnt/9/workspaces/share/test" -if [ -n "${ts:-}" ] && [ -d "$repo" ]; then - mkdir -p "$repo/logs/$ts" || true - echo 0 >"$repo/logs/$ts/guest.exitcode" 2>/dev/null || true - echo "init: OK" >"$repo/logs/$ts/guest.log" 2>/dev/null || true +if [ -d "$repo" ]; then + mkdir -p "$repo/logs" || true + echo 0 >"$repo/logs/guest.exitcode" || true + echo "init: OK" >"$repo/logs/guest.log" || true fi sync || true echo "init: requesting poweroff via sysrq" echo o >/proc/sysrq-trigger || true -echo b >/proc/sysrq-trigger || true # Never exit PID1; if poweroff fails, just idle. echo "init: poweroff returned; idling" diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 364f202..3aac397 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -59,8 +59,18 @@ fi chmod -R a+rX "logs/${TIMESTAMP}" 2>/dev/null || true guest_rc_file="logs/${TIMESTAMP}/guest.exitcode" +guest_rc_src="logs/guest.exitcode" +guest_log_src="logs/guest.log" + +if [ -f "${guest_rc_src}" ]; then + cp -f "${guest_rc_src}" "${guest_rc_file}" 2>/dev/null || true +fi +if [ -f "${guest_log_src}" ]; then + cp -f "${guest_log_src}" "logs/${TIMESTAMP}/guest.log" 2>/dev/null || true +fi + if [ ! -f "${guest_rc_file}" ]; then - echo "WARNING: missing ${guest_rc_file}; treating as failure" + echo "WARNING: missing ${guest_rc_file} (and ${guest_rc_src}); treating as failure" exit 2 fi From c96b3d937a17f6a5e74a38561bceaae8fda521ed Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:56:05 -0500 Subject: [PATCH 65/69] Guest /init: write results under /mnt/9/test The 9p export root is /mnt/9 (bind-mounted repo is /mnt/9/test). Write guest.exitcode/guest.log under that path so v9fs-run-tests can read them. Made-with: Cursor --- scripts/v9fs-build-initrd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 78ae462..f44b18d 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -43,7 +43,7 @@ else echo "init: ERROR: mount hostshare failed" fi -repo="/mnt/9/workspaces/share/test" +repo="/mnt/9/test" if [ -d "$repo" ]; then mkdir -p "$repo/logs" || true echo 0 >"$repo/logs/guest.exitcode" || true From efce5dd55bfb989e57079306b9a6a88d85e54bdd Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 12:56:24 -0500 Subject: [PATCH 66/69] Smoke init: start cpud in background Even though the smoke test mounts 9p directly, we still launch /bbin/cpud in the background for parity with the v9fs/docker initrd behavior. Made-with: Cursor --- scripts/v9fs-build-initrd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index f44b18d..e66eed1 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -25,6 +25,12 @@ mount -t proc proc /proc || true mount -t sysfs sysfs /sys || true mount -t devtmpfs devtmpfs /dev || true +# Best-effort: start cpud in background (not used for smoke path). +if [ -x /bbin/cpud ]; then + echo "init: starting cpud in background" + /bbin/cpud >/dev/console 2>&1 & +fi + mkdir -p /run /tmp /mnt/9 ts="" From 050a9df7ce89a961973ac2419acc438f5b35e66b Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 13:10:00 -0500 Subject: [PATCH 67/69] 9p export: share container root and chroot for smoke Export FSDEV_PATH=/ so the guest can chroot into the container filesystem over 9p and run bash+ls from there. Keep writing guest.exitcode into the repo so the harness still reports failures. Made-with: Cursor --- scripts/v9fs-build-initrd | 12 +++++++++--- scripts/v9fs-run-tests | 10 ++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index e66eed1..7933f86 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -40,16 +40,22 @@ for w in $(cat /proc/cmdline 2>/dev/null || true); do esac done -echo "init: mount+ls 9p smoke ts=${ts:-unset}" +echo "init: mount+ls (chroot) 9p smoke ts=${ts:-unset}" if mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then echo "init: mounted hostshare at /mnt/9" - ls -la /mnt/9 || true + if [ -x /mnt/9/usr/sbin/chroot ] && [ -x /mnt/9/usr/bin/bash ]; then + echo "init: chroot -> /mnt/9, running container bash+ls" + /mnt/9/usr/sbin/chroot /mnt/9 /bin/bash -lc 'set -e; ls -la /; ls -la /home/v9fs-test/test | head' + else + echo "init: missing /usr/sbin/chroot or /bin/bash in exported root; falling back to initrd ls" + ls -la /mnt/9 || true + fi else echo "init: ERROR: mount hostshare failed" fi -repo="/mnt/9/test" +repo="/mnt/9/home/v9fs-test/test" if [ -d "$repo" ]; then mkdir -p "$repo/logs" || true echo 0 >"$repo/logs/guest.exitcode" || true diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 3aac397..1f9e60d 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -12,14 +12,8 @@ mkdir -p "logs/${TIMESTAMP}" export QEMULOG="logs/${TIMESTAMP}/qemu.log" export PIDFILE="/home/v9fs-test/qemu.pid" -# Stable export root so 9p sees our bind mounts. -mkdir -p /workspaces/share -mkdir -p /workspaces/share/test /workspaces/share/tmp /workspaces/share/kernel /workspaces/share/tmpdir -mountpoint -q /workspaces/share/test || mount --bind /home/v9fs-test/test /workspaces/share/test -mountpoint -q /workspaces/share/tmp || mount --bind /workspaces/tmp /workspaces/share/tmp -mountpoint -q /workspaces/share/kernel || mount --bind /workspaces/kernel /workspaces/share/kernel - -export FSDEV_PATH="/workspaces/share" +# Export the whole container root over 9p. +export FSDEV_PATH="/" export KERNELBUILD="${KERNELBUILD:-/workspaces/kernel/.build}" # Patch the image-provided initrd so u-root runs our `/bin/uinit`. From f0da7676d16da7d119f978ba7de8d89050f2c8af Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 13:20:53 -0500 Subject: [PATCH 68/69] CI: add fsx/postmark/dbench stages (guest-direct chroot) Run a CI matrix over smoke, fsx, postmark, and dbench using the new 9p-root export + chroot execution path. Benchmarks are prepared in the container and results are surfaced via guest.exitcode/logs artifacts. Made-with: Cursor --- .github/workflows/ci.yml | 16 ++++++++--- TODO.md | 1 + scripts/v9fs-build-initrd | 39 ++++++++++++++++++-------- scripts/v9fs-prepare-benchmarks | 49 +++++++++++++++++++++++++++++++++ scripts/v9fs-run-tests | 17 +++++++++++- 5 files changed, 106 insertions(+), 16 deletions(-) create mode 100755 scripts/v9fs-prepare-benchmarks diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdfa261..73f8287 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,16 @@ permissions: contents: read jobs: - smoke: + tests: runs-on: ubuntu-24.04-arm + strategy: + fail-fast: false + matrix: + test: + - smoke + - fsx + - postmark + - dbench env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-main' }} @@ -31,7 +39,7 @@ jobs: gh release download "${KERNEL_RELEASE}" -p Image -D kernel/.build/arch/arm64/boot --clobber ls -la kernel/.build/arch/arm64/boot/Image - - name: Run smoke test (guest-direct) + - name: Run ${{ matrix.test }} (guest-direct, chroot) run: | mkdir -p "$GITHUB_WORKSPACE/tmp" docker run --rm --privileged \ @@ -42,7 +50,7 @@ jobs: -v "$GITHUB_WORKSPACE/tmp:/workspaces/tmp" \ -w /home/v9fs-test/test \ "${V9FS_DOCKER_IMAGE}" \ - bash -lc "./scripts/v9fs-run-tests smoke" + bash -lc "./scripts/v9fs-run-tests '${{ matrix.test }}'" - name: Dump logs on failure if: failure() @@ -70,7 +78,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: smoke-logs + name: ${{ matrix.test }}-logs path: logs retention-days: 7 diff --git a/TODO.md b/TODO.md index ab0df21..40b55b5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,7 @@ ## TODO - Extend smoke test into a small suite (create/rename/unlink, fsync, directory traversal, large file IO). +- Add benchmark stages (fsx, postmark, dbench) to guest-direct CI. - Decide whether to adopt a u-root/u-root+cpu initramfs for richer tooling distribution. - Wire `v9fs/linux` to trigger `repository_dispatch` into `linux-kernel-publish.yml`. diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 7933f86..6eb2b40 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -33,33 +33,47 @@ fi mkdir -p /run /tmp /mnt/9 -ts="" -for w in $(cat /proc/cmdline 2>/dev/null || true); do - case "$w" in - v9fs.ts=*) ts="${w#v9fs.ts=}" ;; - esac -done - -echo "init: mount+ls (chroot) 9p smoke ts=${ts:-unset}" +tests="__V9FS_TESTS__" +echo "init: tests=${tests}" if mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; then echo "init: mounted hostshare at /mnt/9" if [ -x /mnt/9/usr/sbin/chroot ] && [ -x /mnt/9/usr/bin/bash ]; then echo "init: chroot -> /mnt/9, running container bash+ls" - /mnt/9/usr/sbin/chroot /mnt/9 /bin/bash -lc 'set -e; ls -la /; ls -la /home/v9fs-test/test | head' + rc=0 + case "${tests}" in + smoke) + /mnt/9/usr/sbin/chroot /mnt/9 /bin/bash -lc 'set -e; ls -la /; ls -la /home/v9fs-test/test | head' || rc=$? + ;; + fsx) + /mnt/9/usr/sbin/chroot /mnt/9 /bin/bash -lc 'set -e; mkdir -p /workspaces/tmpdir; rm -f /workspaces/tmpdir/fsx.testfile; /workspaces/tmp/testbin/fsx/fsx -N 1000 -R -W /workspaces/tmpdir/fsx.testfile' || rc=$? + ;; + postmark) + /mnt/9/usr/sbin/chroot /mnt/9 /bin/bash -lc 'set -e; mkdir -p /workspaces/tmpdir/postmark; cd /workspaces/tmpdir/postmark; /workspaces/tmp/testbin/postmark/postmark <"$repo/logs/guest.exitcode" || true - echo "init: OK" >"$repo/logs/guest.log" || true + echo "${rc:-2}" >"$repo/logs/guest.exitcode" || true + echo "init: tests=${tests} rc=${rc:-2}" >"$repo/logs/guest.log" || true fi sync || true @@ -74,6 +88,9 @@ done EOF chmod +x "$init" +# Bake the selected test into /init (guest environment has no V9FS_TESTS). +sed -i "s/__V9FS_TESTS__/${V9FS_TESTS:-smoke}/g" "$init" + # Collect required userland binaries and their shared libs from the container FS. mkdir -p "$work/rootfs" copy_file() { diff --git a/scripts/v9fs-prepare-benchmarks b/scripts/v9fs-prepare-benchmarks new file mode 100755 index 0000000..8373ba6 --- /dev/null +++ b/scripts/v9fs-prepare-benchmarks @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build/install benchmark binaries into /workspaces/tmp/testbin so they are +# visible to the guest via the QEMU 9p root export (FSDEV_PATH=/). + +root="${TESTBIN_ROOT:-/workspaces/tmp/testbin}" +mkdir -p "${root}"/{fsx,postmark,dbench} + +need_apt=0 +if [ ! -x "${root}/dbench/dbench" ] || [ ! -x "${root}/postmark/postmark" ]; then + need_apt=1 +fi + +if [ "${need_apt}" = "1" ]; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -y + apt-get install -y --no-install-recommends dbench postmark +fi + +if [ ! -x "${root}/dbench/dbench" ]; then + cp -f /usr/bin/dbench "${root}/dbench/dbench" +fi +if [ ! -f "${root}/dbench/client.txt" ]; then + if [ -f /usr/share/dbench/client.txt ]; then + cp -f /usr/share/dbench/client.txt "${root}/dbench/client.txt" + fi +fi + +if [ ! -x "${root}/postmark/postmark" ]; then + cp -f /usr/bin/postmark "${root}/postmark/postmark" +fi + +if [ ! -x "${root}/fsx/fsx" ]; then + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + curl -fsSL \ + https://raw.githubusercontent.com/freebsd/freebsd-src/main/tools/regression/fsx/fsx.c \ + -o "${tmp}/fsx.c" + gcc -O2 -D_GNU_SOURCE -D_POSIX_C_SOURCE=200809L -include stdint.h -include time.h \ + -Wall -Wextra "${tmp}/fsx.c" -o "${root}/fsx/fsx" + chmod +x "${root}/fsx/fsx" +fi + +chmod -R a+rX "${root}" || true + +echo "Prepared benchmarks under ${root}:" +ls -la "${root}/fsx/fsx" "${root}/postmark/postmark" "${root}/dbench/dbench" || true + diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index 1f9e60d..e646840 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -16,12 +16,27 @@ export PIDFILE="/home/v9fs-test/qemu.pid" export FSDEV_PATH="/" export KERNELBUILD="${KERNELBUILD:-/workspaces/kernel/.build}" +# Prepare benchmark binaries when requested. +case "${tests}" in + smoke) ;; + fsx|postmark|dbench) + ./scripts/v9fs-prepare-benchmarks + ;; + *) + echo "ERROR: unknown tests=${tests} (expected smoke|fsx|postmark|dbench)" + exit 2 + ;; +esac + +# Tell v9fs-build-initrd which test to bake in. +export V9FS_TESTS="${tests}" + # Patch the image-provided initrd so u-root runs our `/bin/uinit`. export INITRD="${INITRD:-/opt/v9fs/initrd.patched.cpio}" ./scripts/v9fs-build-initrd "${INITRD}" # Pass timestamp so uinit writes guest.exitcode into logs//. -export EXTRA_APPEND="v9fs.ts=${TIMESTAMP}" +export EXTRA_APPEND="v9fs.ts=${TIMESTAMP} v9fs.tests=${tests}" # Run QEMU daemonized, but stream qemu.log into docker logs. export QEMU_DAEMONIZE=1 From e83b20eadd70677316aef050b986b2a56110fa18 Mon Sep 17 00:00:00 2001 From: Eric Van Hensbergen Date: Sat, 25 Apr 2026 13:38:52 -0500 Subject: [PATCH 69/69] CI: add diod regression stage and docker cleanup Add a diod regression job to the CI matrix and introduce labeled docker runs plus log-capturing cleanup helpers. Made-with: Cursor --- .github/workflows/ci.yml | 3 ++ AGENTS.md | 3 ++ Makefile | 41 +++++++++++---- TODO.md | 1 + scripts/v9fs-build-initrd | 28 ++++++++++ scripts/v9fs-docker-clean | 79 ++++++++++++++++++++++++++++ scripts/v9fs-prepare-diod-regression | 51 ++++++++++++++++++ scripts/v9fs-run-tests | 5 +- 8 files changed, 200 insertions(+), 11 deletions(-) create mode 100755 scripts/v9fs-docker-clean create mode 100755 scripts/v9fs-prepare-diod-regression diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73f8287..33d1f98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: - fsx - postmark - dbench + - diod-regression env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} KERNEL_RELEASE: ${{ inputs.kernel_release || 'kernel-main' }} @@ -43,6 +44,8 @@ jobs: run: | mkdir -p "$GITHUB_WORKSPACE/tmp" docker run --rm --privileged \ + --name "v9fs-test.${{ matrix.test }}.${{ github.run_id }}.${{ github.run_attempt }}" \ + --label v9fs.harness=v9fs-test \ --user 0:0 \ -e KERNELBUILD=/workspaces/kernel/.build \ -v "$GITHUB_WORKSPACE:/home/v9fs-test/test" \ diff --git a/AGENTS.md b/AGENTS.md index 63695d0..6c67004 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,9 @@ Tweak freely. - Primary local dev is **macOS via Docker + QEMU**. - Prefer solutions that work on Docker Desktop (no reliance on KVM). +- After any manual experiment that uses `docker run`, ensure containers are not left running: + - Prefer `docker run --rm` plus a project label `v9fs.harness=v9fs-test`. + - If you suspect a hung run left containers behind, clean up with `make docker-clean` or `./scripts/v9fs-docker-clean`. ## CI architecture preferences - Use http://github.com/v9fs/docker published base image instead of building custom docker for kernel build and/or test frameworks diff --git a/Makefile b/Makefile index 826d660..d1b74a5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: docker-smoke +.PHONY: docker-smoke docker-clean docker-clean-aggressive IMAGE ?= ghcr.io/v9fs/docker:latest KERNEL_RELEASE ?= kernel-main @@ -6,13 +6,34 @@ KERNEL_RELEASE ?= kernel-main docker-smoke: mkdir -p ./tmp ./kernel @echo "Place kernel Image at ./kernel/.build/arch/arm64/boot/Image (or download from release $(KERNEL_RELEASE))" - docker run --rm --privileged \ - --user 0:0 \ - -e KERNELBUILD=/workspaces/kernel/.build \ - -v "$$(pwd):/home/v9fs-test/test" \ - -v "$$(pwd)/kernel:/workspaces/kernel" \ - -v "$$(pwd)/tmp:/workspaces/tmp" \ - -w /home/v9fs-test/test \ - $(IMAGE) \ - bash -lc "./scripts/v9fs-run-tests smoke" + @set -euo pipefail; \ + name="v9fs-test.smoke.$$(date +%s)"; \ + mkdir -p logs/docker; \ + echo "Running container $$name"; \ + set +e; \ + docker run --privileged \ + --name "$$name" \ + --label v9fs.harness=v9fs-test \ + --user 0:0 \ + -e KERNELBUILD=/workspaces/kernel/.build \ + -v "$$(pwd):/home/v9fs-test/test" \ + -v "$$(pwd)/kernel:/workspaces/kernel" \ + -v "$$(pwd)/tmp:/workspaces/tmp" \ + -w /home/v9fs-test/test \ + $(IMAGE) \ + bash -lc "./scripts/v9fs-run-tests smoke"; \ + rc="$$?"; \ + set -e; \ + docker logs "$$name" >"logs/docker/$${name}.log" 2>&1 || true; \ + docker inspect "$$name" >"logs/docker/$${name}.inspect.json" 2>&1 || true; \ + docker rm -f "$$name" >/dev/null 2>&1 || true; \ + echo "Saved docker logs to logs/docker/$${name}.log"; \ + exit "$$rc" + +docker-clean: + @echo "Cleaning containers labeled v9fs.harness=v9fs-test" + @docker ps -aq --filter "label=v9fs.harness=v9fs-test" | xargs -r docker rm -f >/dev/null 2>&1 || true + +docker-clean-aggressive: + @./scripts/v9fs-docker-clean --aggressive diff --git a/TODO.md b/TODO.md index 40b55b5..a6e154e 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,7 @@ - Extend smoke test into a small suite (create/rename/unlink, fsync, directory traversal, large file IO). - Add benchmark stages (fsx, postmark, dbench) to guest-direct CI. +- Add a diod regression suite stage (guest-direct) to exercise the kernel v9fs client. - Decide whether to adopt a u-root/u-root+cpu initramfs for richer tooling distribution. - Wire `v9fs/linux` to trigger `repository_dispatch` into `linux-kernel-publish.yml`. diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index 6eb2b40..373495c 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -54,6 +54,34 @@ if mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/9; th dbench) /mnt/9/usr/sbin/chroot /mnt/9 /bin/bash -lc 'set -e; mkdir -p /workspaces/tmpdir/dbench; /workspaces/tmp/testbin/dbench/dbench -t 30 -c /workspaces/tmp/testbin/dbench/client.txt -D /workspaces/tmpdir/dbench 16' || rc=$? ;; + diod-regression) + # Run a subset of diod's kernel-client regression tests (sharness) inside the guest. + # Build artifacts are prepared on the host side under /workspaces/tmp/diod-build. + /mnt/9/usr/sbin/chroot /mnt/9 /bin/bash -lc ' + set -e + build="$(cat /workspaces/tmp/diod-build.LATEST 2>/dev/null || true)" + if [ -z "$build" ]; then + build="$(ls -d /workspaces/tmp/diod-build-* 2>/dev/null | sort | tail -n 1 || true)" + fi + [ -n "$build" ] || { echo "diod-regression: missing diod build dir"; exit 2; } + cd "$build" + # The sharness tests expect helper binaries from src/cmd check_PROGRAMS. + make -C src/cmd test_diodrun test_flock test_flock_single test_atomic_create test_pathwalk + + # These are the kernel v9fs client oriented tests. + timeout 15m make -C t check TESTS="\ + t0010-v9fs-runasuser.t \ + t0011-v9fs-allsquash.t \ + t0012-v9fs-multiuser.t \ + t0013-v9fs-acl.t \ + t0020-dbench.t \ + t0021-postmark.t \ + " + + # Preserve the automake/sharness combined log in the harness logs dir. + cp -f "$build/t/test-suite.log" /home/v9fs-test/test/logs/diod-test-suite.log 2>/dev/null || true + ' || rc=$? + ;; *) echo "init: ERROR: unknown tests=${tests}" rc=2 diff --git a/scripts/v9fs-docker-clean b/scripts/v9fs-docker-clean new file mode 100755 index 0000000..be09cc9 --- /dev/null +++ b/scripts/v9fs-docker-clean @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: ./scripts/v9fs-docker-clean [--aggressive] + +Default: remove containers labeled v9fs.harness=v9fs-test. +Aggressive: additionally remove any container that has this repo bind-mounted. +Always captures docker logs + inspect before removing. +EOF +} + +aggressive=0 +case "${1:-}" in + "" ) ;; + --aggressive) aggressive=1 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown arg: $1" >&2; usage; exit 2 ;; +esac + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +label="v9fs.harness=v9fs-test" +ts="$(date +%s)" +outdir="${repo_root}/logs/docker/clean-${ts}" +mkdir -p "${outdir}" + +capture_and_remove() { + local id="$1" + local name + name="$(docker inspect -f '{{.Name}}' "$id" 2>/dev/null | sed 's#^/##' || true)" + [ -n "$name" ] || name="$id" + + docker logs "$id" >"${outdir}/${name}.log" 2>&1 || true + docker inspect "$id" >"${outdir}/${name}.inspect.json" 2>&1 || true + docker rm -f "$id" >/dev/null 2>&1 || true +} + +ids=() + +# 1) Labeled containers (always). +while IFS= read -r id; do + [ -n "$id" ] && ids+=("$id") +done < <(docker ps -aq --filter "label=${label}" || true) + +# 2) Aggressive: any container with the repo bind-mounted. +if [ "${aggressive}" = "1" ]; then + while IFS= read -r id; do + [ -n "$id" ] || continue + # Matches containers mounting the repo path anywhere. + if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$id" 2>/dev/null | /usr/bin/grep -Fqx "${repo_root}"; then + ids+=("$id") + fi + done < <(docker ps -aq || true) +fi + +# De-dupe. +uniq_ids=() +seen=" " +for id in "${ids[@]}"; do + case "$seen" in + *" $id "*) continue ;; + *) seen="${seen}${id} "; uniq_ids+=("$id") ;; + esac +done + +if [ "${#uniq_ids[@]}" -eq 0 ]; then + echo "No containers to clean (label=${label}${aggressive:+ or mount=${repo_root}})" + exit 0 +fi + +echo "Capturing logs+inspect to ${outdir}" +echo "Removing ${#uniq_ids[@]} container(s): ${uniq_ids[*]}" +for id in "${uniq_ids[@]}"; do + capture_and_remove "$id" +done + +echo "Done." + diff --git a/scripts/v9fs-prepare-diod-regression b/scripts/v9fs-prepare-diod-regression new file mode 100755 index 0000000..6ebe964 --- /dev/null +++ b/scripts/v9fs-prepare-diod-regression @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Prepare a build tree for the diod sharness regression suite. +# Output is placed under /workspaces/tmp so it is visible to the guest via 9p-root export. + +src="${DIOD_SRC:-/workspaces/tmp/diod-src}" +build_link="${DIOD_BUILD:-/workspaces/tmp/diod-build}" +repo="${DIOD_REPO:-https://github.com/chaos/diod}" +ref="${DIOD_REF:-master}" + +export DEBIAN_FRONTEND=noninteractive +apt-get update -y +apt-get install -y --no-install-recommends \ + autoconf automake libtool pkg-config \ + gcc make \ + lua5.1 liblua5.1-0-dev \ + libcap-dev libmunge-dev libwrap0-dev \ + tclsh rsync perl \ + sudo \ + acl \ + dbench postmark + +# Clone/update source. +if [ ! -d "${src}/.git" ]; then + rm -rf "${src}" + git clone --depth 1 "${repo}" "${src}" +fi +git -C "${src}" fetch --depth 1 origin "${ref}" || true +git -C "${src}" checkout -q "${ref}" || true + +# Autotools bootstrap. +(cd "${src}" && ./autogen.sh) + +# Build in a content-addressed directory (avoids cleanup issues with mixed uid mappings). +build_real="/workspaces/tmp/diod-build-$(git -C "${src}" rev-parse --short HEAD)" +mkdir -p "${build_real}" + +(cd "${build_real}" && "${src}/configure") +(cd "${build_real}" && make -j"$(nproc)") + +# Record the build directory for the guest (symlinks can be awkward across 9p+bind mounts). +echo "${build_real}" > /workspaces/tmp/diod-build.LATEST + +# The guest writes test logs into the build tree; ensure it's writable over 9p +# regardless of uid mapping. +chmod -R a+rwX "${src}" "${build_real}" || true + +echo "Prepared diod build at ${build_real}" +ls -la "${build_real}/t" | head -n 50 || true + diff --git a/scripts/v9fs-run-tests b/scripts/v9fs-run-tests index e646840..2e3b8d2 100755 --- a/scripts/v9fs-run-tests +++ b/scripts/v9fs-run-tests @@ -22,8 +22,11 @@ case "${tests}" in fsx|postmark|dbench) ./scripts/v9fs-prepare-benchmarks ;; + diod-regression) + ./scripts/v9fs-prepare-diod-regression + ;; *) - echo "ERROR: unknown tests=${tests} (expected smoke|fsx|postmark|dbench)" + echo "ERROR: unknown tests=${tests} (expected smoke|fsx|postmark|dbench|diod-regression)" exit 2 ;; esac