name: Build kernel module on: # Release flow: triggered when a version tag is pushed (tag a commit on master). # Builds every target and publishes the modules to a Gitea release. push: tags: - 'v*' # Compile-check flow: every PR that targets master. Builds only, no release. pull_request: branches: - master jobs: # Build (and thereby compile-check) the module for each pinned kernel version. # Runs on both triggers — on a PR this is the whole story; on a tag the # `release` job below consumes the artifacts. # # HEADERS_PKG is fixed to linux-headers-rpi-v8 (64-bit, Pi Zero 2 W): the -v6/-v7 # headers are 32-bit (armhf) and aren't carried by the arm64 raspberrypi archive # this arm64 builder uses. build: runs-on: ubuntu-latest env: HEADERS_PKG: linux-headers-rpi-v8 strategy: # Don't cancel the other versions if one fails to build. fail-fast: false matrix: # Each raspberrypi archive suite only carries its own latest kernel, so # the Debian base suite is paired with the kernel version it can install: # bookworm -> 6.12.x, trixie -> 6.18.x. include: - kernel_version: "1:6.12.93-1+rpt1" suite: bookworm - kernel_version: "1:6.18.34-1+rpt1" suite: trixie steps: - name: Checkout uses: actions/checkout@v4 - name: Build module (${{ matrix.kernel_version }}) working-directory: kernel env: HEADERS_PKG: ${{ env.HEADERS_PKG }} KERNEL_VERSION: ${{ matrix.kernel_version }} DEBIAN_SUITE: ${{ matrix.suite }} run: | set -eux # arm64 emulation for the builder image (no-op if already registered). if [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]; then docker run --privileged --rm tonistiigi/binfmt --install arm64 fi # Per-version image tag. The matrix entries can run concurrently on a # single runner sharing one Docker daemon; a fixed tag (e.g. iec-kbuild) # is then a shared mutable name and the builds race — whichever `docker # build` finishes last wins the tag, so both `docker create` calls # resolve to the same image and every job emits the same vermagic. # A unique tag per kernel version isolates them. Docker tags allow only # [a-zA-Z0-9._-], so map ':' '/' '+' (all present in 1:6.12.93-1+rpt1) # to '-'; the tag just needs to be unique and valid, not reversible. img="iec-kbuild:${KERNEL_VERSION//[:\/+]/-}" # Builder image: toolchain + matching raspberrypi kernel headers. docker build --platform linux/arm64 \ --build-arg DEBIAN_SUITE="$DEBIAN_SUITE" \ --build-arg HEADERS_PKG="$HEADERS_PKG" \ --build-arg KERNEL_VERSION="$KERNEL_VERSION" \ -t "$img" . # Compile inside the container. We use `docker cp` instead of the bind # mount that build-in-docker.sh uses for local builds: under the runner's # docker-in-docker, /workspace is a volume the host daemon can't see, so # `-v "$PWD:/build"` mounts an empty dir and make finds no Makefile. cid=$(docker create --platform linux/arm64 --entrypoint sleep "$img" infinity) docker start "$cid" docker cp ./. "$cid:/build" docker exec "$cid" /usr/local/bin/docker-entrypoint.sh clean docker exec "$cid" /usr/local/bin/docker-entrypoint.sh docker cp "$cid:/build/iec_listener.ko" ./iec_listener.ko docker rm -f "$cid" docker rmi "$img" || true - name: Stage build output (modules/iec_listener_.ko) working-directory: kernel run: | ver="${{ matrix.kernel_version }}" safe="${ver//[:\/]/-}" # path-friendly: 1:6.12.93-1+rpt1 -> 1-6.12.93-1+rpt1 echo "kernel version $ver -> file modules/iec_listener_${safe}.ko" mkdir -p out/modules mv iec_listener.ko "out/modules/iec_listener_${safe}.ko" # Expose the path-friendly name for the (colon-rejecting) artifact name. echo "SAFE_VER=$safe" >> "$GITHUB_ENV" - name: Upload build output uses: https://github.com/christopherHX/gitea-upload-artifact@v4 with: # Name by the path-friendly version so artifact names stay unique; all # of them merge back into the single modules/ folder at download time. name: ${{ env.SAFE_VER }} path: kernel/out if-no-files-found: error # Merge all built modules into one modules/ folder, wrap it in a single # repo-named top folder alongside the helper scripts (selftest.sh, launch.sh) # and the iecpoc Python frontend (so launch.sh can pip-install it on the Pi). # Runs on BOTH triggers and uploads the assembled FOLDER as an artifact: on a PR # this lets you download and smoke-test the exact tree a release would ship; on a # tag the release job below zips this same tree and attaches it. # # We intentionally upload the folder, not a pre-made .zip: artifacts are always # transported as a zip by the platform, so uploading a .zip would nest one zip # inside another. Uploading the tree makes the platform's wrapper zip *be* the # package archive (a single, clean layer). package: needs: build runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Download all build output uses: https://github.com/christopherHX/gitea-download-artifact@v4 with: path: dist # Merge every per-kernel artifact back into the shared modules/ folder. merge-multiple: true - name: Determine version label id: ver run: | # Tags ship under the tag name; PR builds get a traceable pr-. case "${{ github.ref }}" in refs/tags/*) v="${{ github.ref_name }}" ;; *) v="pr${{ github.event.pull_request.number }}-$(echo "${{ github.sha }}" | cut -c1-7)" ;; esac echo "ver=$v" >> "$GITHUB_OUTPUT" - name: Assemble package tree run: | repo="${{ github.event.repository.name }}" root="package/$repo" mkdir -p "$root" cp -r dist/modules "$root"/ # modules/iec_listener_.ko cp kernel/selftest.sh "$root"/ # one-shot hardware self-test cp launch.sh "$root"/ # load module -> run frontend -> unload # iecpoc Python frontend + its packaging metadata. launch.sh pip-installs # this on the Pi; the build reads pyproject.toml's readme = "README.md", # so README.md must travel with it. __pycache__ is stripped to stay clean. cp pyproject.toml README.md "$root"/ cp -r iecpoc "$root"/ find "$root/iecpoc" -name __pycache__ -type d -prune -exec rm -rf {} + - name: Upload package uses: https://github.com/christopherHX/gitea-upload-artifact@v4 with: name: ${{ github.event.repository.name }}-${{ steps.ver.outputs.ver }} # Upload the contents of package/, so the artifact zip contains the # single top folder package// -> /... path: package if-no-files-found: error # Release flow only: download the assembled tree the package job uploaded, zip it # into the named release asset and attach it to the Gitea release for the pushed # tag. No re-assembly here (the tree is taken as-is). Skipped on PRs. release: needs: package if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - name: Download package uses: https://github.com/christopherHX/gitea-download-artifact@v4 with: # The single artifact the package job produced for this tag; extracts to # dist//... name: ${{ github.event.repository.name }}-${{ github.ref_name }} path: dist - name: Zip release asset run: | repo="${{ github.event.repository.name }}" (cd dist && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo") - name: Publish to Gitea release uses: https://gitea.com/actions/gitea-release-action@v1.3.6 with: api_key: ${{ secrets.GITEA_TOKEN }} files: |- ${{ github.event.repository.name }}-${{ github.ref_name }}.zip