CI/pipeline: - KERNEL_TARBALL passed as a YAML env literal '~' was never tilde-expanded and would have failed every hosted kernel-build dispatch; the path is now exported from the shell. Verified reproducible before the fix. - Every job gets timeout-minutes; boot smoke uses timeout -k so a wedged qemu is SIGKILLed instead of holding the job. - Tarball fetch + fail-closed sha256 verification deduplicated into build/fetch-kernel-tarball.sh (with curl retries), used by build-kernel.sh and both CI jobs. busybox fetch gains retries too. - ccache layer for kernel-build (cache keyed on defconfig+patches) recovers the incremental-compile speed the ephemeral-runner move cost. - build-kernel.sh now asserts every fragment option survived olddefconfig — merge_config -m pastes text and Kconfig silently drops unmet symbols. rs485-bridge: - pending-buffer cap (2x max RTU ADU) instead of unbounded growth; explicit accept-loop error handling with backoff instead of .flatten(); per-arm inline bounds instead of the string-keyed lookup whose default would have mis-bounded a future get-input; control-socket cleanup errors surfaced; flag-shaped values rejected in arg parsing; doc example uses a private mktemp dir. Test timing margins widened for contended runners (gap 25->120ms, 60x margin on the split-frame test). VM harness: - stage-1/stage-2 boot scripts share one validated slot parser and one by-name populator (qemu/rootfs/etc/warden-lib.sh) — the duplicated parser had already diverged on validation; userdata/oem mount failures now fail fast with a greppable sentinel; udhcpc fallback keys off the interface actually having an address; switch_root applet guarded. - boot-smoke delegates the qemu invocation to run.sh (machine shape lives in ONE place); run.sh port 0 disables a hostfwd. - mkimage: unknown partition names fail at build time; DISK_END is a max, not last-entry; --state keys validated as filenames. - portal-scenario: mock readiness is asserted (no silent fall-through), hostfwd port collisions retried, mount-failure sentinel fails fast. - ui-shot: fixed sleeps replaced with bounded screendump polling; the repaint assertion is real and documented as such. qmp.py loses its module-global and gains argv validation. Docs/scrub: bench-host paths and the site AP name removed from six more port docs and two evidence tables; path-bearing build artifacts (.elf, .map) untracked (the 154-byte firmware .bin is path-free and stays); ADR-0003 marked visibility-superseded by ADR-0007; stale section cross-reference fixed; flare-edge noted as private for outside readers; stale root-level review report removed per the new workspace rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
159 lines
6.6 KiB
Bash
Executable File
159 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Hermetic-ish kernel build for the WardenOS 86-Panel (RV1106).
|
|
#
|
|
# fetch pristine linux-6.18.46 -> apply patches/ in order -> configure with
|
|
# build/warden_defconfig -> build zImage + rv1106-warden.dtb.
|
|
#
|
|
# The pristine tree is the ONLY external input; the delta is patches/. A build
|
|
# from a clean checkout + a verified tarball is reproducible.
|
|
#
|
|
# Env:
|
|
# KERNEL_TARBALL path to a local linux-6.18.46.tar.xz (skips the download)
|
|
# SDK_TC dir holding the arm-rockchip830 uclibc cross toolchain bin/
|
|
# WORK build scratch dir (default: a mktemp under $TMPDIR)
|
|
# JOBS parallel make jobs (default: nproc)
|
|
# WARDEN_KCONFIG_FRAGMENT
|
|
# optional kconfig fragment merged onto warden_defconfig
|
|
# (qemu/configs/virt.fragment builds the QEMU -M virt variant);
|
|
# every fragment option is verified to have taken effect
|
|
# WARDEN_CCACHE=1 compile through ccache (CI caches ~/.ccache)
|
|
#
|
|
# Requires: `python` (not python3) on PATH — the SDK quirk; the CI runner provides
|
|
# a project-local venv. Builds are SERIAL on the shared SDK box — never run two.
|
|
set -euo pipefail
|
|
|
|
KVER=6.18.46
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # build/
|
|
REPO="$(cd "$HERE/.." && pwd)"
|
|
PATCHES="$REPO/patches"
|
|
JOBS="${JOBS:-$(nproc)}"
|
|
# The tarball URL + sha256 pin live in fetch-kernel-tarball.sh (shared with CI).
|
|
|
|
# A caller-provided WORK (e.g. CI's ${{ github.workspace }}/kbuild-out, from which
|
|
# artifacts are uploaded) is left intact; a scratch dir we mktemp'd here is our own
|
|
# (>1GB of extracted source + build output) and is removed on exit so repeated runs
|
|
# on the shared SDK box don't fill the disk.
|
|
if [ -n "${WORK:-}" ]; then
|
|
WORK_OWNED=0
|
|
else
|
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/warden-kbuild.XXXXXX")"
|
|
WORK_OWNED=1
|
|
fi
|
|
trap '[ "${WORK_OWNED:-0}" = 1 ] && rm -rf "$WORK"' EXIT
|
|
|
|
log() { printf '\033[36m== %s\033[0m\n' "$*"; }
|
|
|
|
command -v python >/dev/null || { echo "need 'python' (not python3) on PATH — SDK quirk" >&2; exit 1; }
|
|
|
|
# 1. obtain + verify the pristine tarball
|
|
mkdir -p "$WORK"
|
|
TB="${KERNEL_TARBALL:-$WORK/linux-$KVER.tar.xz}"
|
|
# Fetch + fail-closed sha256 verification live in ONE place shared with CI
|
|
# (a missing pin or a mismatch always refuses to build).
|
|
bash "$HERE/fetch-kernel-tarball.sh" "$TB"
|
|
|
|
# 2. extract pristine
|
|
SRC="$WORK/linux-$KVER"
|
|
rm -rf "$SRC"
|
|
log "extracting pristine"
|
|
tar -C "$WORK" -xf "$TB"
|
|
|
|
# 3. apply the patch series in order (fail loudly — never echo a lie)
|
|
log "applying patch series"
|
|
for p in "$PATCHES"/*.patch; do
|
|
if git -C "$SRC" apply --whitespace=nowarn "$p" 2>/dev/null; then
|
|
:
|
|
elif patch -d "$SRC" -p1 --forward --no-backup-if-mismatch < "$p" >/dev/null 2>&1; then
|
|
:
|
|
else
|
|
echo "FATAL: failed to apply $(basename "$p")" >&2
|
|
exit 1
|
|
fi
|
|
echo " applied $(basename "$p")"
|
|
done
|
|
|
|
# Guard against a SILENT no-op: `git apply` run from inside another git repo's
|
|
# subdirectory ignores out-of-subdir paths and exits 0 without applying anything
|
|
# (issue #1). $WORK must therefore live OUTSIDE any git checkout. Assert that a known
|
|
# product of the series actually landed on disk, so this can never masquerade as
|
|
# success again.
|
|
SENTINEL="$SRC/arch/arm/boot/dts/rockchip/rv1106-warden.dts"
|
|
[ -f "$SENTINEL" ] || {
|
|
echo "FATAL: patch series did not apply (missing $SENTINEL)." >&2
|
|
echo " Is \$WORK inside a git repo? git apply silently ignores out-of-subdir" >&2
|
|
echo " paths there — point WORK at a dir outside any checkout (e.g. \$RUNNER_TEMP)." >&2
|
|
exit 1
|
|
}
|
|
log "patch series applied ($(basename "$SENTINEL") present)"
|
|
|
|
# 4. configure
|
|
log "configuring (warden_defconfig)"
|
|
cp "$HERE/warden_defconfig" "$SRC/.config"
|
|
# Optional kconfig fragment overlay (e.g. qemu/configs/virt.fragment for the
|
|
# QEMU -M virt device-sim variant). Fail closed if set but unreadable — never
|
|
# silently build the wrong kernel. Unset => the canonical RV1106 build,
|
|
# byte-identical to a build without this hook.
|
|
if [ -n "${WARDEN_KCONFIG_FRAGMENT:-}" ]; then
|
|
[ -f "$WARDEN_KCONFIG_FRAGMENT" ] || {
|
|
echo "FATAL: WARDEN_KCONFIG_FRAGMENT set but not a file: $WARDEN_KCONFIG_FRAGMENT" >&2
|
|
exit 1
|
|
}
|
|
FRAG="$(cd "$(dirname "$WARDEN_KCONFIG_FRAGMENT")" && pwd)/$(basename "$WARDEN_KCONFIG_FRAGMENT")"
|
|
log "merging kconfig fragment $(basename "$FRAG")"
|
|
( cd "$SRC" && ARCH=arm ./scripts/kconfig/merge_config.sh -m .config "$FRAG" )
|
|
fi
|
|
# CROSS_COMPILE defaults to the Luckfox SDK uclibc prefix (set SDK_TC to its bin/),
|
|
# but the kernel is freestanding, so a caller may override with a generic arm cross
|
|
# toolchain instead — e.g. CROSS_COMPILE=arm-linux-gnueabihf- (in Debian's
|
|
# gcc-arm-linux-gnueabihf), which the CI runner already has on PATH.
|
|
export ARCH=arm
|
|
export CROSS_COMPILE="${CROSS_COMPILE:-arm-rockchip830-linux-uclibcgnueabihf-}"
|
|
if [ -n "${SDK_TC:-}" ]; then export PATH="$SDK_TC:$PATH"; fi
|
|
command -v "${CROSS_COMPILE}gcc" >/dev/null \
|
|
|| { echo "cross toolchain ${CROSS_COMPILE}gcc not on PATH (set SDK_TC, or CROSS_COMPILE to one that is)" >&2; exit 1; }
|
|
make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" olddefconfig >/dev/null
|
|
|
|
# Fragment took-effect assertion: merge_config -m only pastes text, and
|
|
# olddefconfig silently resolves any symbol whose dependencies are unmet —
|
|
# a fragment option could be dropped without a word. Verify every explicit
|
|
# request in the fragment survived into the final .config; fail loud if not.
|
|
if [ -n "${WARDEN_KCONFIG_FRAGMENT:-}" ]; then
|
|
frag_fail=0
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
CONFIG_*=*)
|
|
grep -qxF "$line" "$SRC/.config" || {
|
|
echo "FATAL: fragment option '$line' did not take effect (unmet Kconfig dependency?)" >&2
|
|
frag_fail=1
|
|
} ;;
|
|
"# CONFIG_"*" is not set")
|
|
opt="${line#\# }"; opt="${opt% is not set}"
|
|
grep -qE "^$opt=" "$SRC/.config" && {
|
|
echo "FATAL: fragment disabled '$opt' but it is set in the final .config" >&2
|
|
frag_fail=1
|
|
} ;;
|
|
esac
|
|
done < "$FRAG"
|
|
[ "$frag_fail" = 0 ] || exit 1
|
|
log "fragment options verified in final .config"
|
|
fi
|
|
|
|
# Optional ccache (CI: cache ~/.ccache across dispatches; harmless if unset).
|
|
KCC="${CROSS_COMPILE}gcc"
|
|
if [ "${WARDEN_CCACHE:-0}" = 1 ]; then
|
|
command -v ccache >/dev/null || { echo "FATAL: WARDEN_CCACHE=1 but ccache not installed" >&2; exit 1; }
|
|
KCC="ccache ${CROSS_COMPILE}gcc"
|
|
fi
|
|
|
|
# 5. build zImage + the board dtb
|
|
log "building zImage + rv1106-warden.dtb (-j$JOBS)"
|
|
make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" CC="$KCC" -j"$JOBS" \
|
|
zImage rockchip/rv1106-warden.dtb
|
|
|
|
Z="$SRC/arch/arm/boot/zImage"
|
|
D="$SRC/arch/arm/boot/dts/rockchip/rv1106-warden.dtb"
|
|
log "build OK"
|
|
ls -la "$Z" "$D"
|
|
echo "zImage: $Z"
|
|
echo "dtb: $D"
|