mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 22:26:33 +00:00
Phase 1 of the OS OTA pipeline. Three pieces:
scripts/gen-rauc-signing-keys.sh — one-shot helper that issues an
Ed25519 X.509 CA + signing cert pair. Operator runs locally, commits
the CA cert (for embedding in kiosk image at /etc/rauc/keyring.pem),
stores the signing pair as GitHub Actions secrets
(BF_RAUC_SIGNING_CERT + BF_RAUC_SIGNING_KEY), keeps the CA private
key offline. RAUC verifies bundles against the keyring in the image.
deploy/rauc/build-bundle.sh — takes the pi-gen .img.xz, parses its
partition table with sfdisk, dd-extracts bootfs (vfat) + rootfs
(ext4) into a staging dir, renders manifest.raucm.in with version
+ git sha, runs `rauc bundle --cert= --key=` to produce a signed
.raucb. Verifies the bundle round-trips with `rauc info`.
build.yml gains two gated steps:
- "Build RAUC bundle": runs only when both signing secrets are set,
uploads .raucb as a release asset alongside the .img.xz.
- "Auto-import OS bundle into BF server": POSTs the GH release asset
URL to ${BF_AUTOIMPORT_URL}/api/admin/os/import so the server
pulls + stores the bundle. Mirrors the kiosk-binary auto-import
flow that already worked.
Compatibility string is `betterframe-rpi5-aarch64` (matches the value
already declared in deploy/rauc/system.conf). Channel passed through
from inputs (dev for master pushes, stable/beta for tags).
What's NOT in this commit:
- Pi image A/B partition layout (custom genimage / pi-gen patch)
- rauc package install + keyring drop in pi-gen stage
- Kiosk-side os_update.rs Rust consumer that polls /api/kiosk/os/check
- Admin UI for releases + rollouts
A bundle built today reaches /api/admin/os/import on the server but
isn't installable yet — kiosks have no consumer and no A/B layout.
That's the next 3 phases. Bundle production needs to be solid first
so the kiosk side can be tested against real artifacts.
109 lines
3.7 KiB
Bash
Executable file
109 lines
3.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Build a signed RAUC .raucb bundle from a pi-gen-produced .img.xz.
|
|
#
|
|
# Usage:
|
|
# build-bundle.sh <input.img.xz> <output.raucb> <version> <git_sha> \
|
|
# <signing_cert.pem> <signing_key.pem>
|
|
#
|
|
# Approach: decompress the .img.xz, identify its bootfs (vfat) + rootfs
|
|
# (ext4) partitions via sfdisk, dd them into bundle-staging/ as
|
|
# bootfs.vfat + rootfs.ext4, render the manifest template with version
|
|
# + git sha, then `rauc bundle --cert= --key= staging out.raucb`.
|
|
#
|
|
# We use the FAT and ext4 partitions from a stock pi-gen image — i.e. the
|
|
# bundle content matches what's on a freshly-flashed kiosk. The TARGET
|
|
# device still needs an A/B partition layout for RAUC to actually install
|
|
# (separate workstream); a bundle built today is only consumable by
|
|
# kiosks already running the A/B layout.
|
|
set -euo pipefail
|
|
|
|
IN_IMG_XZ="${1:?input .img.xz required}"
|
|
OUT_RAUCB="${2:?output .raucb path required}"
|
|
VERSION="${3:?version required}"
|
|
GIT_SHA="${4:?git sha required}"
|
|
SIGNING_CERT="${5:?signing cert path required}"
|
|
SIGNING_KEY="${6:?signing key path required}"
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
MANIFEST_IN="${SCRIPT_DIR}/manifest.raucm.in"
|
|
|
|
WORK_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
|
|
|
echo "==> Decompressing $IN_IMG_XZ"
|
|
RAW_IMG="${WORK_DIR}/image.img"
|
|
xz -d -c "$IN_IMG_XZ" > "$RAW_IMG"
|
|
|
|
echo "==> Reading partition table"
|
|
# sfdisk -d emits: <device>: start=N, size=N, type=X, name=...
|
|
# pi-gen layout: p1 = bootfs (vfat, type=c), p2 = rootfs (ext4, type=83)
|
|
BOOT_INFO="$(sfdisk -d "$RAW_IMG" | awk '/img1/ || /img.*: start/ {print}')"
|
|
ROOT_INFO="$(sfdisk -d "$RAW_IMG" | awk '/img2/ || /img.*: start/ {print}')"
|
|
|
|
# Robust parse: walk partition lines, identify by type code.
|
|
parse_part() {
|
|
local part_idx="$1"
|
|
sfdisk -d "$RAW_IMG" \
|
|
| awk -v idx="$part_idx" '
|
|
/: start=/ {
|
|
n++;
|
|
if (n == idx) {
|
|
for (i = 1; i <= NF; i++) {
|
|
if ($i ~ /start=/) { gsub(/[^0-9]/, "", $i); start = $i }
|
|
if ($i ~ /size=/) { gsub(/[^0-9]/, "", $i); size = $i }
|
|
}
|
|
print start, size;
|
|
exit
|
|
}
|
|
}'
|
|
}
|
|
|
|
read BOOT_START BOOT_SIZE < <(parse_part 1)
|
|
read ROOT_START ROOT_SIZE < <(parse_part 2)
|
|
if [ -z "${BOOT_START:-}" ] || [ -z "${ROOT_START:-}" ]; then
|
|
echo "could not parse pi-gen partition table — expected 2 partitions" >&2
|
|
sfdisk -d "$RAW_IMG"
|
|
exit 1
|
|
fi
|
|
|
|
echo " bootfs: start=$BOOT_START size=$BOOT_SIZE sectors (512B each)"
|
|
echo " rootfs: start=$ROOT_START size=$ROOT_SIZE sectors (512B each)"
|
|
|
|
STAGE="${WORK_DIR}/bundle"
|
|
mkdir -p "$STAGE"
|
|
|
|
echo "==> Extracting bootfs.vfat"
|
|
dd if="$RAW_IMG" of="${STAGE}/bootfs.vfat" \
|
|
bs=512 skip="$BOOT_START" count="$BOOT_SIZE" status=none
|
|
|
|
echo "==> Extracting rootfs.ext4"
|
|
dd if="$RAW_IMG" of="${STAGE}/rootfs.ext4" \
|
|
bs=512 skip="$ROOT_START" count="$ROOT_SIZE" status=none
|
|
|
|
# Shrink the rootfs to actual used space so bundles don't ship empty bytes.
|
|
# pi-gen's export-image already does this, but verify file integrity first.
|
|
echo "==> Checking rootfs.ext4 integrity"
|
|
e2fsck -fy "${STAGE}/rootfs.ext4" || true # tolerate "clean but old fs version" warnings
|
|
|
|
echo "==> Rendering manifest"
|
|
sed -e "s|@VERSION@|${VERSION}|g" \
|
|
-e "s|@GIT_SHA@|${GIT_SHA}|g" \
|
|
"$MANIFEST_IN" > "${STAGE}/manifest.raucm"
|
|
|
|
echo "==> Bundle staging contents"
|
|
ls -la "$STAGE"
|
|
cat "${STAGE}/manifest.raucm"
|
|
|
|
echo "==> Building RAUC bundle"
|
|
rm -f "$OUT_RAUCB"
|
|
rauc bundle \
|
|
--cert="$SIGNING_CERT" \
|
|
--key="$SIGNING_KEY" \
|
|
"$STAGE" "$OUT_RAUCB"
|
|
|
|
echo "==> Verifying bundle (uses the signing cert as its own trust anchor)"
|
|
rauc info --keyring="$SIGNING_CERT" "$OUT_RAUCB"
|
|
|
|
echo
|
|
echo "==> Bundle written: $OUT_RAUCB"
|
|
ls -la "$OUT_RAUCB"
|