From 25dd8e1f2895303feddb87545cf06fd48652dd01 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 17:48:47 +0200 Subject: [PATCH 01/11] Add Gitea Actions build workflow for rpilinux-image Host-based kas build on a dedicated 'yocto:host' runner with a persistent project dir ($HOME/yocto-ci) for incremental sstate reuse. Uploads deploy images from raspberrypi4-64 as artifacts. --- .gitea/workflows/build.yaml | 91 +++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .gitea/workflows/build.yaml diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..98267fd --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,91 @@ +name: Build RPi image + +# Yocto (kas) build of rpilinux-image for raspberrypi4-64. +# +# Runner requirements (see chat/notes): +# - label "yocto:host" (host job: Yocto needs ~250GB persistent disk, +# container jobs are not suitable) +# - ~250GB free in $HOME, plus the poky host dependencies + node +# - runner.timeout >= 8h for the first build (runner default is 3h) + +on: + push: + branches: [main] + workflow_dispatch: + +# Never run two builds at once on the same (single) runner. +concurrency: + group: yocto-rpi-build + cancel-in-progress: false + +permissions: + contents: read + +jobs: + build: + runs-on: yocto + timeout-minutes: 600 + steps: + - uses: builtin:checkout + + - name: Probe host dependencies + run: | + set -e + missing="" + for t in git python3 curl wget rsync gawk xz diffstat chrpath node npm; do + command -v "$t" >/dev/null 2>&1 || missing="$missing $t" + done + if [ -n "$missing" ]; then + echo "::error::Missing host tools:$missing" + echo "Install on the runner host, e.g.:" + echo ' sudo apt-get install -y --no-install-recommends build-essential \ + bison chrpath curl diffstat file gawk git libpython3-dev libssl-dev \ + make python3 python3-venv python3-pip python3-dev rsync swig unzip \ + wget xz-utils zstd bzip2 nodejs npm' + exit 1 + fi + + - name: Set up kas (persistent venv) + run: | + set -e + export YOCTO_HOME="$HOME/yocto-ci" + mkdir -p "$YOCTO_HOME" + [ -d "$YOCTO_HOME/venv" ] || python3 -m venv "$YOCTO_HOME/venv" + "$YOCTO_HOME/venv/bin/pip" -q install -U kas + "$YOCTO_HOME/venv/bin/kas" --version + + - name: Sync to persistent project dir and build + run: | + set -euo pipefail + export YOCTO_HOME="$HOME/yocto-ci" + # Persistent project dir: keeps extern/ (layer clones) and build/ + # (sstate + tmp) between runs -> incremental builds after the first. + rsync -a --delete \ + --exclude .git --exclude extern --exclude build \ + ./ "$YOCTO_HOME/yocto_rpi/" + cd "$YOCTO_HOME/yocto_rpi" + git config --global --add safe.directory "$YOCTO_HOME/yocto_rpi" || true + "$YOCTO_HOME/venv/bin/kas" build + + - name: Collect images + run: | + set -euo pipefail + export YOCTO_HOME="$HOME/yocto-ci" + cd "$YOCTO_HOME/yocto_rpi" + IMG_DIR="build/*/tmp/deploy/images/raspberrypi4-64" + ls -lh $IMG_DIR + mkdir -p "$GITEA_WORKSPACE/dist" + cp $IMG_DIR/* "$GITEA_WORKSPACE/dist/" + { + echo "## RPi4 image built" + echo + echo "- commit: ${GITEA_SHA}" + echo "- images also on the runner host at: \`$YOCTO_HOME/yocto_rpi/$IMG_DIR\`" + } >> "$GITEA_STEP_SUMMARY" + + - name: Upload images + uses: actions/upload-artifact@v4 + with: + name: rpilinux-image-rpi4 + path: dist/ + retention-days: 14 -- 2.56.0 From 439b5c53a8d89ac9652a1cff904dbf6db77d6bf6 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 17:58:43 +0200 Subject: [PATCH 02/11] Match instance runner label; probe environment on first run --- .gitea/workflows/build.yaml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 98267fd..294fcbf 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -3,10 +3,15 @@ name: Build RPi image # Yocto (kas) build of rpilinux-image for raspberrypi4-64. # # Runner requirements (see chat/notes): -# - label "yocto:host" (host job: Yocto needs ~250GB persistent disk, -# container jobs are not suitable) +# - matches the instance runner's "ubuntu-latest" label (bare label = job +# runs on the runner's machine, i.e. a host job). Yocto needs ~250GB +# persistent disk, so the runner must actually be a machine (or a +# container with a big persistent volume), not a throwaway sandbox. # - ~250GB free in $HOME, plus the poky host dependencies + node # - runner.timeout >= 8h for the first build (runner default is 3h) +# +# Upgrade path: register a dedicated runner with label "yocto:host" and +# change runs-on below to "yocto". on: push: @@ -23,13 +28,21 @@ permissions: jobs: build: - runs-on: yocto + runs-on: ubuntu-latest timeout-minutes: 600 steps: - uses: builtin:checkout - - name: Probe host dependencies + - name: Probe environment and host dependencies run: | + echo "=== runner environment ===" + uname -a + [ -r /etc/os-release ] && . /etc/os-release && echo "$PRETTY_NAME" + echo "HOME=$HOME nproc=$(nproc)" + df -h "$HOME" | tail -1 + free -h 2>/dev/null | sed -n 2p || true + command -v docker >/dev/null && docker info --format 'docker: {{.ServerVersion}}' 2>/dev/null || echo "no docker daemon" + echo "=== dependency check ===" set -e missing="" for t in git python3 curl wget rsync gawk xz diffstat chrpath node npm; do -- 2.56.0 From 4a645bab2d452a8439cf067929dff77c0ce8ff41 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 18:18:01 +0200 Subject: [PATCH 03/11] Target dedicated host runner (yocto label); YOCTO_HOME overridable via runner env --- .gitea/workflows/build.yaml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 294fcbf..5f67843 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -2,16 +2,13 @@ name: Build RPi image # Yocto (kas) build of rpilinux-image for raspberrypi4-64. # -# Runner requirements (see chat/notes): -# - matches the instance runner's "ubuntu-latest" label (bare label = job -# runs on the runner's machine, i.e. a host job). Yocto needs ~250GB -# persistent disk, so the runner must actually be a machine (or a -# container with a big persistent volume), not a throwaway sandbox. -# - ~250GB free in $HOME, plus the poky host dependencies + node -# - runner.timeout >= 8h for the first build (runner default is 3h) +# Runs on the dedicated host runner (label "yocto:host", Ryzen 9 box). +# Everything persists in $YOCTO_HOME: extern/ (layer clones) + build/ (sstate) +# survive between runs -> incremental builds after the first. +# $YOCTO_HOME defaults to $HOME/yocto-ci; override it via runner.envs +# (runner.envs.YOCTO_HOME=/data/yocto-ci) to put it on the big disk. # -# Upgrade path: register a dedicated runner with label "yocto:host" and -# change runs-on below to "yocto". +# First build takes hours (poky fetch + full image) -> runner.timeout >= 8h. on: push: @@ -28,7 +25,7 @@ permissions: jobs: build: - runs-on: ubuntu-latest + runs-on: yocto timeout-minutes: 600 steps: - uses: builtin:checkout @@ -52,7 +49,7 @@ jobs: echo "::error::Missing host tools:$missing" echo "Install on the runner host, e.g.:" echo ' sudo apt-get install -y --no-install-recommends build-essential \ - bison chrpath curl diffstat file gawk git libpython3-dev libssl-dev \ + bison flex chrpath curl diffstat file gawk git libpython3-dev libssl-dev \ make python3 python3-venv python3-pip python3-dev rsync swig unzip \ wget xz-utils zstd bzip2 nodejs npm' exit 1 @@ -61,7 +58,7 @@ jobs: - name: Set up kas (persistent venv) run: | set -e - export YOCTO_HOME="$HOME/yocto-ci" + export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-ci}" mkdir -p "$YOCTO_HOME" [ -d "$YOCTO_HOME/venv" ] || python3 -m venv "$YOCTO_HOME/venv" "$YOCTO_HOME/venv/bin/pip" -q install -U kas @@ -70,7 +67,7 @@ jobs: - name: Sync to persistent project dir and build run: | set -euo pipefail - export YOCTO_HOME="$HOME/yocto-ci" + export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-ci}" # Persistent project dir: keeps extern/ (layer clones) and build/ # (sstate + tmp) between runs -> incremental builds after the first. rsync -a --delete \ @@ -83,7 +80,7 @@ jobs: - name: Collect images run: | set -euo pipefail - export YOCTO_HOME="$HOME/yocto-ci" + export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-ci}" cd "$YOCTO_HOME/yocto_rpi" IMG_DIR="build/*/tmp/deploy/images/raspberrypi4-64" ls -lh $IMG_DIR -- 2.56.0 From 31264c17c9d50b224b90c40fbfe9481d1c1f2dd8 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 18:35:05 +0200 Subject: [PATCH 04/11] Separate runner dir from persistent build dir; guard against self-sync --- .gitea/workflows/build.yaml | 40 +++++++++++++++++++++++-------------- .zed/settings.json | 5 +++++ 2 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 .zed/settings.json diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 5f67843..4f9abbf 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -2,11 +2,13 @@ name: Build RPi image # Yocto (kas) build of rpilinux-image for raspberrypi4-64. # -# Runs on the dedicated host runner (label "yocto:host", Ryzen 9 box). +# Runs on the dedicated host runner (label "yocto:host", Ryzen 9 box "ada"). # Everything persists in $YOCTO_HOME: extern/ (layer clones) + build/ (sstate) # survive between runs -> incremental builds after the first. -# $YOCTO_HOME defaults to $HOME/yocto-ci; override it via runner.envs -# (runner.envs.YOCTO_HOME=/data/yocto-ci) to put it on the big disk. +# $YOCTO_HOME comes from the runner config (runner.envs.YOCTO_HOME = +# /home/bl/yocto-build, on the 1.8T disk); falls back to $HOME/yocto-build. +# +# First build takes hours (poky fetch + full image) -> runner.timeout >= 8h. # # First build takes hours (poky fetch + full image) -> runner.timeout >= 8h. @@ -35,7 +37,7 @@ jobs: echo "=== runner environment ===" uname -a [ -r /etc/os-release ] && . /etc/os-release && echo "$PRETTY_NAME" - echo "HOME=$HOME nproc=$(nproc)" + echo "HOME=$HOME workspace=$GITEA_WORKSPACE nproc=$(nproc)" df -h "$HOME" | tail -1 free -h 2>/dev/null | sed -n 2p || true command -v docker >/dev/null && docker info --format 'docker: {{.ServerVersion}}' 2>/dev/null || echo "no docker daemon" @@ -58,29 +60,37 @@ jobs: - name: Set up kas (persistent venv) run: | set -e - export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-ci}" + export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-build}" mkdir -p "$YOCTO_HOME" [ -d "$YOCTO_HOME/venv" ] || python3 -m venv "$YOCTO_HOME/venv" "$YOCTO_HOME/venv/bin/pip" -q install -U kas "$YOCTO_HOME/venv/bin/kas" --version - - name: Sync to persistent project dir and build + - name: Sync to persistent build dir and build run: | set -euo pipefail - export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-ci}" - # Persistent project dir: keeps extern/ (layer clones) and build/ - # (sstate + tmp) between runs -> incremental builds after the first. - rsync -a --delete \ - --exclude .git --exclude extern --exclude build \ - ./ "$YOCTO_HOME/yocto_rpi/" - cd "$YOCTO_HOME/yocto_rpi" - git config --global --add safe.directory "$YOCTO_HOME/yocto_rpi" || true + export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-build}" + SRC="$(pwd)" + DST="$YOCTO_HOME/yocto_rpi" + # Safety: builtin:checkout cleans ignored files (build/, extern/), + # so the build tree must NOT be the ephemeral workspace. + if [ "$SRC" = "$DST" ]; then + echo "::error::workspace == persistent dir ($SRC); keep the runner WorkingDirectory outside YOCTO_HOME" + exit 1 + fi + echo "syncing $SRC -> $DST" + mkdir -p "$DST" + # .git is copied (kas expects a git repo); extern/ and build/ exist + # only in DST and are excluded, so --delete cannot touch them. + rsync -a --delete --exclude /build/ --exclude /extern/ ./ "$DST/" + cd "$DST" + git config --global --add safe.directory "$DST" || true "$YOCTO_HOME/venv/bin/kas" build - name: Collect images run: | set -euo pipefail - export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-ci}" + export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-build}" cd "$YOCTO_HOME/yocto_rpi" IMG_DIR="build/*/tmp/deploy/images/raspberrypi4-64" ls -lh $IMG_DIR diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..79860b3 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,5 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{} -- 2.56.0 From a71b18828957b44480dfe51d54daa0e753abb64a Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 18:47:37 +0200 Subject: [PATCH 05/11] Use https for meta-openembedded (git:// port 9418 unreachable) --- .config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config.yaml b/.config.yaml index ec1fc8a..acfa2dc 100644 --- a/.config.yaml +++ b/.config.yaml @@ -24,7 +24,7 @@ repos: meta-poky: meta-yocto-bsp: meta-openembedded: - url: git://git.openembedded.org/meta-openembedded + url: https://git.openembedded.org/meta-openembedded tag: scarthgap path: extern/ layers: -- 2.56.0 From 5396dd1d7eb85fa0f560d060d2f80958a83dfef0 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 18:52:23 +0200 Subject: [PATCH 06/11] Give each kas repo its own path (all three shared extern/, clobbering each other) --- .config.yaml | 12 +++++++----- .gitea/workflows/build.yaml | 14 ++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.config.yaml b/.config.yaml index acfa2dc..4c1babf 100644 --- a/.config.yaml +++ b/.config.yaml @@ -12,13 +12,13 @@ target: rpilinux-image repos: meta-raspberrypi: - url: http://git.yoctoproject.org/git/meta-raspberrypi - path: extern/ + url: https://git.yoctoproject.org/git/meta-raspberrypi + path: extern/meta-raspberrypi tag: scarthgap poky: - url: http://git.yoctoproject.org/git/poky + url: https://git.yoctoproject.org/git/poky tag: scarthgap - path: extern/ + path: extern/poky layers: meta: meta-poky: @@ -26,7 +26,7 @@ repos: meta-openembedded: url: https://git.openembedded.org/meta-openembedded tag: scarthgap - path: extern/ + path: extern/meta-openembedded layers: meta-oe: meta-multimedia: @@ -34,6 +34,8 @@ repos: meta-python: local: path: meta-rpilinux + local: + path: meta-rpilinux local_conf_header: ipk: | diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 4f9abbf..22f1e9f 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -92,15 +92,21 @@ jobs: set -euo pipefail export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-build}" cd "$YOCTO_HOME/yocto_rpi" - IMG_DIR="build/*/tmp/deploy/images/raspberrypi4-64" - ls -lh $IMG_DIR + IMG_DIR="$(find build -type d -path '*/tmp/deploy/images/raspberrypi4-64' | head -1)" + if [ -z "$IMG_DIR" ]; then + echo "::error::no deploy dir found under build/" + exit 1 + fi + echo "images in: $IMG_DIR" + ls -lh "$IMG_DIR" mkdir -p "$GITEA_WORKSPACE/dist" - cp $IMG_DIR/* "$GITEA_WORKSPACE/dist/" + # only the image artefacts, not the million symlinks/cache dirs + find "$IMG_DIR" -maxdepth 1 -type f \( -name '*.wic*' -o -name '*.ext4*' -o -name '*.rootfs*' -o -name '*.tar.bz2' -o -name '*.dtb' -o -name 'Image*' -o -name 'modules*' \) -exec cp -av {} "$GITEA_WORKSPACE/dist/" \; { echo "## RPi4 image built" echo echo "- commit: ${GITEA_SHA}" - echo "- images also on the runner host at: \`$YOCTO_HOME/yocto_rpi/$IMG_DIR\`" + echo "- build tree on runner host: \`$YOCTO_HOME/yocto_rpi/$IMG_DIR\`" } >> "$GITEA_STEP_SUMMARY" - name: Upload images -- 2.56.0 From 571c8ad70c24179ceb103f940a0e8c14ea807b57 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 19:14:09 +0200 Subject: [PATCH 07/11] Fix artifact collection: workspace-relative dist, canonical image names, checksums --- .gitea/workflows/build.yaml | 39 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 22f1e9f..3bfcf9f 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -37,7 +37,7 @@ jobs: echo "=== runner environment ===" uname -a [ -r /etc/os-release ] && . /etc/os-release && echo "$PRETTY_NAME" - echo "HOME=$HOME workspace=$GITEA_WORKSPACE nproc=$(nproc)" + echo "HOME=$HOME workspace=$(pwd) nproc=$(nproc)" df -h "$HOME" | tail -1 free -h 2>/dev/null | sed -n 2p || true command -v docker >/dev/null && docker info --format 'docker: {{.ServerVersion}}' 2>/dev/null || echo "no docker daemon" @@ -91,6 +91,7 @@ jobs: run: | set -euo pipefail export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-build}" + WS="$(pwd)" # workspace; upload-artifact's path is relative to it cd "$YOCTO_HOME/yocto_rpi" IMG_DIR="$(find build -type d -path '*/tmp/deploy/images/raspberrypi4-64' | head -1)" if [ -z "$IMG_DIR" ]; then @@ -98,16 +99,32 @@ jobs: exit 1 fi echo "images in: $IMG_DIR" - ls -lh "$IMG_DIR" - mkdir -p "$GITEA_WORKSPACE/dist" - # only the image artefacts, not the million symlinks/cache dirs - find "$IMG_DIR" -maxdepth 1 -type f \( -name '*.wic*' -o -name '*.ext4*' -o -name '*.rootfs*' -o -name '*.tar.bz2' -o -name '*.dtb' -o -name 'Image*' -o -name 'modules*' \) -exec cp -av {} "$GITEA_WORKSPACE/dist/" \; - { - echo "## RPi4 image built" - echo - echo "- commit: ${GITEA_SHA}" - echo "- build tree on runner host: \`$YOCTO_HOME/yocto_rpi/$IMG_DIR\`" - } >> "$GITEA_STEP_SUMMARY" + mkdir -p "$WS/dist" + shopt -s nullglob + # only the timestamped canonical names - the short "...rootfs.wic.bz2" + # entries are hardlinks to the same inode (would double the upload) + FILES=("$IMG_DIR"/rpilinux-image-*.rootfs-[0-9]*.wic.bz2 "$IMG_DIR"/rpilinux-image-*.rootfs-[0-9]*.wic.bmap) + if [ ${#FILES[@]} -eq 0 ]; then + echo "::error::no flashable .wic image found in $IMG_DIR" + exit 1 + fi + cp -v "${FILES[@]}" "$WS/dist/" + ( cd "$WS/dist" && sha256sum * > SHA256SUMS ) + ls -lh "$WS/dist" + SUMMARY="${GITEA_STEP_SUMMARY:-${GITHUB_STEP_SUMMARY:-}}" + if [ -n "$SUMMARY" ]; then + { + echo "## RPi4 image built" + echo + echo "| file | sha256 |" + echo "|---|---|" + ( cd "$WS/dist" && while read -r sum f; do echo "| \`$f\` | \`${sum:0:16}\` |"; done < SHA256SUMS ) + echo + echo "Flash: \`bzcat | sudo dd of=/dev/sdX bs=4M status=progress\`" + echo + echo "Build tree (incremental) on runner host: \`$YOCTO_HOME/yocto_rpi\`" + } >> "$SUMMARY" + fi - name: Upload images uses: actions/upload-artifact@v4 -- 2.56.0 From 6f8456f2b68479235a6842547cd6aa0553f9b086 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 19:30:09 +0200 Subject: [PATCH 08/11] Trigger CI on ci/* branches so main can stay protected --- .gitea/workflows/build.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 3bfcf9f..deca5e4 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -14,7 +14,8 @@ name: Build RPi image on: push: - branches: [main] + # main is release-protected; CI branches may be pushed freely for testing + branches: [main, 'ci/*'] workflow_dispatch: # Never run two builds at once on the same (single) runner. -- 2.56.0 From d4ca8fe7c591c1d28d771328e59945644e73f4a3 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 19:31:07 +0200 Subject: [PATCH 09/11] Add pull_request trigger so PRs into main post a status check --- .gitea/workflows/build.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index deca5e4..23b3c94 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -14,8 +14,12 @@ name: Build RPi image on: push: - # main is release-protected; CI branches may be pushed freely for testing + # main is release-protected; ci/* branches are free scratch space branches: [main, 'ci/*'] + pull_request: + # gates merges into main, and provides the commit status that a + # "require status checks" branch-protection rule waits for + branches: [main] workflow_dispatch: # Never run two builds at once on the same (single) runner. -- 2.56.0 From 16d63d95cb93865999d134d890540be746285678 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 19:39:31 +0200 Subject: [PATCH 10/11] Refactor CI into composite actions + repo variables The workflow was mostly inline bash. Moves the logic into reusable composite actions under .gitea/actions/, keeps host paths in the YOCTO_HOME repo variable, adds a workflow_dispatch skip-build option and publishes step outputs (build dir, image list, kas path). --- .gitea/actions/build-image/action.yml | 117 ++++++++++++++++ .gitea/actions/check-build-host/action.yml | 37 +++++ .gitea/actions/setup-kas/action.yml | 34 +++++ .gitea/workflows/build.yaml | 153 ++++++--------------- 4 files changed, 233 insertions(+), 108 deletions(-) create mode 100644 .gitea/actions/build-image/action.yml create mode 100644 .gitea/actions/check-build-host/action.yml create mode 100644 .gitea/actions/setup-kas/action.yml diff --git a/.gitea/actions/build-image/action.yml b/.gitea/actions/build-image/action.yml new file mode 100644 index 0000000..78488af --- /dev/null +++ b/.gitea/actions/build-image/action.yml @@ -0,0 +1,117 @@ +name: build-image +description: >- + Build a kas project in a persistent directory and expose the flashable + images. The workspace is synced into the build dir instead of built in + place, because checkout cleans ignored files (build/, extern/) and bitbake + reuse depends on sstate-cache surviving between runs. +inputs: + project: + description: kas configuration file, relative to the repository + required: false + default: .config.yaml + machine: + description: Machine the images are deployed for + required: false + default: raspberrypi4-64 + image-prefix: + description: Artifact filename prefix in the deploy dir + required: false + default: rpilinux-image + skip-build: + description: Set to true to only collect images from an existing build dir + required: false + default: "false" +outputs: + build-dir: + description: Persistent project directory on the runner + value: ${{ steps.locate.outputs.build-dir }} + image-dir: + description: Deploy dir bitbake wrote to + value: ${{ steps.locate.outputs.image-dir }} + images: + description: Newline separated artifact filenames + value: ${{ steps.collect.outputs.images }} +runs: + using: composite + steps: + - name: Sync workspace into persistent build dir + if: ${{ inputs.skip-build != 'true' }} + shell: bash + run: | + set -euo pipefail + SRC="$(pwd)" + DST="${YOCTO_HOME:-$HOME/yocto-build}/$(basename "$GITHUB_REPOSITORY")" + # checkout deletes ignored files, so the build tree must live elsewhere + if [ "$SRC" = "$DST" ]; then + echo "::error::workspace == build dir ($SRC); point YOCTO_HOME somewhere else" + exit 1 + fi + echo "syncing $SRC -> $DST" + mkdir -p "$DST" + # .git stays (kas requires a repo); extern/ + build/ only exist in DST, + # so --delete cannot reach them + rsync -a --delete --exclude /build/ --exclude /extern/ ./ "$DST/" + + - name: kas build + if: ${{ inputs.skip-build != 'true' }} + shell: bash + run: | + set -euo pipefail + cd "${YOCTO_HOME:-$HOME/yocto-build}/$(basename "$GITHUB_REPOSITORY")" + git config --global --add safe.directory "$PWD" || true + "${YOCTO_HOME:-$HOME/yocto-build}/venv/bin/kas" build + + - name: Locate deploy dir + id: locate + shell: bash + run: | + set -euo pipefail + BUILD_DIR="${YOCTO_HOME:-$HOME/yocto-build}/$(basename "$GITHUB_REPOSITORY")" + IMAGE_DIR="$(find "$BUILD_DIR/build" -type d -path "*/tmp/deploy/images/${{ inputs.machine }}" | head -1)" + if [ -z "$IMAGE_DIR" ]; then + echo "::error::no deploy dir for ${{ inputs.machine }} under $BUILD_DIR/build" + exit 1 + fi + echo "build-dir=$BUILD_DIR" >> "$GITHUB_OUTPUT" + echo "image-dir=$IMAGE_DIR" >> "$GITHUB_OUTPUT" + + - name: Collect artifacts + id: collect + shell: bash + run: | + set -euo pipefail + IMAGE_DIR="${{ steps.locate.outputs.image-dir }}" + shopt -s nullglob + # only the timestamped names: the short ones are hardlinks to the same + # inode and would upload every image twice + FILES=("${IMAGE_DIR}/${{ inputs.image-prefix }}."*.rootfs-[0-9]*.wic.* \ + "${IMAGE_DIR}/${{ inputs.image-prefix }}-*.rootfs-[0-9]*.wic.*") + if [ "${#FILES[@]}" -eq 0 ]; then + echo "::error::no flashable .wic image for ${{ inputs.machine }} in $IMAGE_DIR" + exit 1 + fi + mkdir -p dist + cp -v "${FILES[@]}" dist/ + ( cd dist && sha256sum * > SHA256SUMS ) + ls -lh dist + printf 'images=%s\n' "$(ls dist | tr '\n' ' ')" >> "$GITHUB_OUTPUT" + + - name: Summarise + shell: bash + run: | + set -euo pipefail + SUMMARY="${GITHUB_STEP_SUMMARY:-}" + [ -n "$SUMMARY" ] || exit 0 + { + echo "## ${{ inputs.image-prefix }} for ${{ inputs.machine }}" + echo + echo "| artifact | sha256 |" + echo "| --- | --- |" + ( cd dist && while read -r sum file; do + [ "$file" = SHA256SUMS ] || echo "| \`$file\` | \`${sum:0:16}\` |" + done < SHA256SUMS ) + echo + echo "Flash with \`bzcat | sudo dd of=/dev/sdX bs=4M status=progress\`." + echo + echo "Incremental build tree: \`${{ steps.locate.outputs.build-dir }}\`" + } >> "$SUMMARY" diff --git a/.gitea/actions/check-build-host/action.yml b/.gitea/actions/check-build-host/action.yml new file mode 100644 index 0000000..db54030 --- /dev/null +++ b/.gitea/actions/check-build-host/action.yml @@ -0,0 +1,37 @@ +name: check-build-host +description: >- + Report the build host and fail fast when executables, CPU or disk that the + build needs are missing - a mistake should cost seconds, not hours. +inputs: + tools: + description: Space separated executables that must be on PATH + required: false + default: git python3 + min-disk-gb: + description: Warn when the build filesystem has less than this much free + required: false + default: "100" +runs: + using: composite + steps: + - name: Host report + shell: bash + run: | + set -euo pipefail + echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -m), $(uname -r))" + echo "user : $(id -un) home=$HOME" + echo "cpu : $(nproc) cores / $(awk '/MemTotal/{printf "%.0fG", $2/1048576}' /proc/meminfo) ram" + df -h . | tail -1 | sed 's/^/disk : /' + missing="" + for tool in ${{ inputs.tools }}; do + command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool" + done + if [ -n "$missing" ]; then + echo "::error::Host is missing:$missing" + echo "Install them on the runner host, e.g. sudo apt-get install -y $missing" + exit 1 + fi + free_gb="$(df --output=avail -BG . | tail -1 | tr -d ' ')" + if [ "$free_gb" -lt "${{ inputs.min-disk-gb }}" ]; then + echo "::warning::only ${free_gb}G free on $(df --output=target . | tail -1); expected >= ${{ inputs.min-disk-gb }}G" + fi diff --git a/.gitea/actions/setup-kas/action.yml b/.gitea/actions/setup-kas/action.yml new file mode 100644 index 0000000..1859f92 --- /dev/null +++ b/.gitea/actions/setup-kas/action.yml @@ -0,0 +1,34 @@ +name: setup-kas +description: Create or reuse the persistent python venv that holds kas. +inputs: + requirement: + description: pip requirement for kas (pinned, e.g. kas==5.5) + required: false + default: kas +outputs: + kas: + description: Absolute path of the kas executable + value: ${{ steps.venv.outputs.kas }} + reused: + description: "true when the venv already existed (warm run)" + value: ${{ steps.venv.outputs.reused }} +runs: + using: composite + steps: + - name: Prepare venv + id: venv + shell: bash + run: | + set -euo pipefail + BUILD_ROOT="${YOCTO_HOME:-$HOME/yocto-build}" + VENV="$BUILD_ROOT/venv" + mkdir -p "$BUILD_ROOT" + if [ -d "$VENV" ]; then + echo "reused=true" >> "$GITHUB_OUTPUT" + else + echo "reused=false" >> "$GITHUB_OUTPUT" + python3 -m venv "$VENV" + fi + "$VENV/bin/pip" install --quiet --upgrade "${{ inputs.requirement }}" + echo "kas=$VENV/bin/kas" >> "$GITHUB_OUTPUT" + echo "kas $($VENV/bin/kas --version) in $VENV" diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 23b3c94..a8d566f 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -1,28 +1,29 @@ name: Build RPi image -# Yocto (kas) build of rpilinux-image for raspberrypi4-64. +# Yocto (kas) build of rpilinux-image. Steps live in .gitea/actions/* so this +# file stays a description of the pipeline, not a shell script. # -# Runs on the dedicated host runner (label "yocto:host", Ryzen 9 box "ada"). -# Everything persists in $YOCTO_HOME: extern/ (layer clones) + build/ (sstate) -# survive between runs -> incremental builds after the first. -# $YOCTO_HOME comes from the runner config (runner.envs.YOCTO_HOME = -# /home/bl/yocto-build, on the 1.8T disk); falls back to $HOME/yocto-build. -# -# First build takes hours (poky fetch + full image) -> runner.timeout >= 8h. -# -# First build takes hours (poky fetch + full image) -> runner.timeout >= 8h. +# Host-specific paths come from repo settings (Settings -> Actions -> +# Variables), so the workflow is not tied to one machine: +# YOCTO_HOME persistent build dir on the runner +# Order of precedence for the build dir: this variable, then the runner's +# own YOCTO_HOME (runner.envs), then $HOME/yocto-build. on: push: - # main is release-protected; ci/* branches are free scratch space + # main is release protected, ci/* branches are free scratch space branches: [main, 'ci/*'] pull_request: - # gates merges into main, and provides the commit status that a - # "require status checks" branch-protection rule waits for + # gates merges into main and posts the commit status that branch + # protection waits for branches: [main] workflow_dispatch: + inputs: + skip-build: + description: Collect images from the existing build dir instead of rebuilding + type: boolean + default: false -# Never run two builds at once on the same (single) runner. concurrency: group: yocto-rpi-build cancel-in-progress: false @@ -30,110 +31,46 @@ concurrency: permissions: contents: read +env: + YOCTO_HOME: ${{ vars.YOCTO_HOME }} + MACHINE: raspberrypi4-64 + IMAGE_PREFIX: rpilinux-image + +defaults: + run: + shell: bash + jobs: build: runs-on: yocto timeout-minutes: 600 steps: - - uses: builtin:checkout + - name: Check out sources + uses: builtin:checkout - - name: Probe environment and host dependencies - run: | - echo "=== runner environment ===" - uname -a - [ -r /etc/os-release ] && . /etc/os-release && echo "$PRETTY_NAME" - echo "HOME=$HOME workspace=$(pwd) nproc=$(nproc)" - df -h "$HOME" | tail -1 - free -h 2>/dev/null | sed -n 2p || true - command -v docker >/dev/null && docker info --format 'docker: {{.ServerVersion}}' 2>/dev/null || echo "no docker daemon" - echo "=== dependency check ===" - set -e - missing="" - for t in git python3 curl wget rsync gawk xz diffstat chrpath node npm; do - command -v "$t" >/dev/null 2>&1 || missing="$missing $t" - done - if [ -n "$missing" ]; then - echo "::error::Missing host tools:$missing" - echo "Install on the runner host, e.g.:" - echo ' sudo apt-get install -y --no-install-recommends build-essential \ - bison flex chrpath curl diffstat file gawk git libpython3-dev libssl-dev \ - make python3 python3-venv python3-pip python3-dev rsync swig unzip \ - wget xz-utils zstd bzip2 nodejs npm' - exit 1 - fi + - name: Verify build host + uses: $/.gitea/actions/check-build-host + with: + tools: git python3 curl wget rsync gawk xz diffstat chrpath bison flex node + min-disk-gb: "200" - - name: Set up kas (persistent venv) - run: | - set -e - export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-build}" - mkdir -p "$YOCTO_HOME" - [ -d "$YOCTO_HOME/venv" ] || python3 -m venv "$YOCTO_HOME/venv" - "$YOCTO_HOME/venv/bin/pip" -q install -U kas - "$YOCTO_HOME/venv/bin/kas" --version + - name: Set up kas + id: kas + uses: $/.gitea/actions/setup-kas + with: + requirement: kas==5.5 - - name: Sync to persistent build dir and build - run: | - set -euo pipefail - export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-build}" - SRC="$(pwd)" - DST="$YOCTO_HOME/yocto_rpi" - # Safety: builtin:checkout cleans ignored files (build/, extern/), - # so the build tree must NOT be the ephemeral workspace. - if [ "$SRC" = "$DST" ]; then - echo "::error::workspace == persistent dir ($SRC); keep the runner WorkingDirectory outside YOCTO_HOME" - exit 1 - fi - echo "syncing $SRC -> $DST" - mkdir -p "$DST" - # .git is copied (kas expects a git repo); extern/ and build/ exist - # only in DST and are excluded, so --delete cannot touch them. - rsync -a --delete --exclude /build/ --exclude /extern/ ./ "$DST/" - cd "$DST" - git config --global --add safe.directory "$DST" || true - "$YOCTO_HOME/venv/bin/kas" build + - name: Build image + id: image + uses: $/.gitea/actions/build-image + with: + machine: ${{ env.MACHINE }} + image-prefix: ${{ env.IMAGE_PREFIX }} + skip-build: ${{ github.event.inputs.skip-build }} - - name: Collect images - run: | - set -euo pipefail - export YOCTO_HOME="${YOCTO_HOME:-$HOME/yocto-build}" - WS="$(pwd)" # workspace; upload-artifact's path is relative to it - cd "$YOCTO_HOME/yocto_rpi" - IMG_DIR="$(find build -type d -path '*/tmp/deploy/images/raspberrypi4-64' | head -1)" - if [ -z "$IMG_DIR" ]; then - echo "::error::no deploy dir found under build/" - exit 1 - fi - echo "images in: $IMG_DIR" - mkdir -p "$WS/dist" - shopt -s nullglob - # only the timestamped canonical names - the short "...rootfs.wic.bz2" - # entries are hardlinks to the same inode (would double the upload) - FILES=("$IMG_DIR"/rpilinux-image-*.rootfs-[0-9]*.wic.bz2 "$IMG_DIR"/rpilinux-image-*.rootfs-[0-9]*.wic.bmap) - if [ ${#FILES[@]} -eq 0 ]; then - echo "::error::no flashable .wic image found in $IMG_DIR" - exit 1 - fi - cp -v "${FILES[@]}" "$WS/dist/" - ( cd "$WS/dist" && sha256sum * > SHA256SUMS ) - ls -lh "$WS/dist" - SUMMARY="${GITEA_STEP_SUMMARY:-${GITHUB_STEP_SUMMARY:-}}" - if [ -n "$SUMMARY" ]; then - { - echo "## RPi4 image built" - echo - echo "| file | sha256 |" - echo "|---|---|" - ( cd "$WS/dist" && while read -r sum f; do echo "| \`$f\` | \`${sum:0:16}\` |"; done < SHA256SUMS ) - echo - echo "Flash: \`bzcat | sudo dd of=/dev/sdX bs=4M status=progress\`" - echo - echo "Build tree (incremental) on runner host: \`$YOCTO_HOME/yocto_rpi\`" - } >> "$SUMMARY" - fi - - - name: Upload images + - name: Upload image uses: actions/upload-artifact@v4 with: - name: rpilinux-image-rpi4 + name: ${{ env.IMAGE_PREFIX }}-${{ env.MACHINE }} path: dist/ retention-days: 14 -- 2.56.0 From 1c596cf097426fc4d7d41f634b2cc86992fc06f3 Mon Sep 17 00:00:00 2001 From: Bo Lorentsen Date: Sat, 24 Oct 2026 19:41:59 +0200 Subject: [PATCH 11/11] Keep image globs outside quotes so they expand --- .gitea/actions/build-image/action.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitea/actions/build-image/action.yml b/.gitea/actions/build-image/action.yml index 78488af..f890ae1 100644 --- a/.gitea/actions/build-image/action.yml +++ b/.gitea/actions/build-image/action.yml @@ -83,9 +83,10 @@ runs: IMAGE_DIR="${{ steps.locate.outputs.image-dir }}" shopt -s nullglob # only the timestamped names: the short ones are hardlinks to the same - # inode and would upload every image twice + # inode and would upload every image twice. + # NOTE: the glob must stay OUTSIDE the quotes - "$DIR/pat*" never expands. FILES=("${IMAGE_DIR}/${{ inputs.image-prefix }}."*.rootfs-[0-9]*.wic.* \ - "${IMAGE_DIR}/${{ inputs.image-prefix }}-*.rootfs-[0-9]*.wic.*") + "${IMAGE_DIR}/${{ inputs.image-prefix }}-"*.rootfs-[0-9]*.wic.*) if [ "${#FILES[@]}" -eq 0 ]; then echo "::error::no flashable .wic image for ${{ inputs.machine }} in $IMAGE_DIR" exit 1 -- 2.56.0