diff --git a/alpine/build-rootfs.sh b/alpine/build-rootfs.sh index a50ad6b..a57e359 100755 --- a/alpine/build-rootfs.sh +++ b/alpine/build-rootfs.sh @@ -50,12 +50,122 @@ rm -r "$ROOTFS/build" rm "$ROOTFS/etc/resolv.conf" ### Begin making full disk image for the device -echo "##################################################### Packaging RootFS "$( du -sh "$ROOTFS/" ) +#!/bin/bash +set -euo pipefail -IMG=output.img -SIZE=8GB +ROOTFS="${ROOTFS:?ROOTFS is required}" +BOARD_ITB="${BOARD_ITB:-/build/board.itb}" -dd if=/dev/zero of="$IMG" bs=1 count=0 seek=$SIZE +IMG="${IMG:-output.img}" +DISK_SIZE="${DISK_SIZE:-8G}" + +ROOTFS_IMG="${ROOTFS_IMG:-rootfs.ext4}" +ROOTFS_IMG_ZST="${ROOTFS_IMG_ZST:-rootfs.ext4.zst}" +ROOTFS_PART_SIZE_MIB="${ROOTFS_PART_SIZE_MIB:-2048}" + +FAKE_DEV="/tmp/dev" +MNT_ROOTFS_IMG="/mnt/rootfs-img" +MNT_DATA="/mnt/data" + +LOOP="" +ROOTFS_LOOP="" +TMP_LOOP="" +ROOTFS_TMP_LOOP="" + +cleanup_fake_nodes() { + local prefix="$1" + [ -n "$prefix" ] || return 0 + find "$FAKE_DEV" -maxdepth 1 -type b -name "${prefix}*" -exec rm -f {} \; 2>/dev/null || true +} + +cleanup() { + set +e + + mountpoint -q "$MNT_ROOTFS_IMG" && umount "$MNT_ROOTFS_IMG" + mountpoint -q "$MNT_DATA" && umount "$MNT_DATA" + + if [ -n "$ROOTFS_LOOP" ]; then + losetup -d "$ROOTFS_LOOP" 2>/dev/null || true + fi + if [ -n "$LOOP" ]; then + losetup -d "$LOOP" 2>/dev/null || true + fi + + if [ -n "$ROOTFS_LOOP" ]; then + cleanup_fake_nodes "$(basename "$ROOTFS_LOOP")" + fi + if [ -n "$LOOP" ]; then + cleanup_fake_nodes "$(basename "$LOOP")" + fi +} +trap cleanup EXIT + +mkdir -p "$FAKE_DEV" "$MNT_ROOTFS_IMG" "$MNT_DATA" + +echo "##################################################### Packaging RootFS $(du -sh "$ROOTFS" | awk '{print $1}')" + +############################################################################### +# 1. Build reusable rootfs ext4 image once +############################################################################### + +ROOTFS_BYTES=$(du -s -B1 "$ROOTFS" | awk '{print $1}') +EXTRA_BYTES=$((256 * 1024 * 1024)) +IMG_BYTES=$(( ROOTFS_BYTES + ROOTFS_BYTES / 4 + EXTRA_BYTES )) + +ALIGN=$((4 * 1024 * 1024)) +IMG_BYTES=$(( (IMG_BYTES + ALIGN - 1) / ALIGN * ALIGN )) + +MAX_BYTES=$(( ROOTFS_PART_SIZE_MIB * 1024 * 1024 )) +if [ "$IMG_BYTES" -ge "$MAX_BYTES" ]; then + echo "ERROR: estimated rootfs image size $IMG_BYTES exceeds slot size $MAX_BYTES" >&2 + exit 1 +fi + +rm -f "$ROOTFS_IMG" "$ROOTFS_IMG_ZST" + +truncate -s "$IMG_BYTES" "$ROOTFS_IMG" +mkfs.ext4 -F -L rootfs "$ROOTFS_IMG" + +ROOTFS_LOOP=$(losetup --find --show -P "$ROOTFS_IMG") +/sync-loop.sh "$ROOTFS_LOOP" + +# For a raw ext4 image there is usually no partition, so mount the loop device directly. +mount "$ROOTFS_LOOP" "$MNT_ROOTFS_IMG" + +( + cd "$ROOTFS" + tar cpf - --exclude='./var' . +) | ( + cd "$MNT_ROOTFS_IMG" + tar xpf - +) + +mkdir -p "$MNT_ROOTFS_IMG/var" +mkdir -p "$MNT_ROOTFS_IMG/boot" +cp "$BOARD_ITB" "$MNT_ROOTFS_IMG/boot/kernel.itb" + +sync +umount "$MNT_ROOTFS_IMG" + +losetup -d "$ROOTFS_LOOP" +cleanup_fake_nodes "$(basename "$ROOTFS_LOOP")" +ROOTFS_LOOP="" + +e2fsck -fy "$ROOTFS_IMG" +resize2fs -M "$ROOTFS_IMG" +e2fsck -fy "$ROOTFS_IMG" + +echo "##################################################### Compressing OTA Image" +zstd -19 -T0 -f "$ROOTFS_IMG" -o "$ROOTFS_IMG_ZST" +sha256sum "$ROOTFS_IMG" > "$ROOTFS_IMG.sha256" +sha256sum "$ROOTFS_IMG_ZST" > "$ROOTFS_IMG_ZST.sha256" + +############################################################################### +# 2. Build full disk image +############################################################################### + +rm -f "$IMG" +truncate -s "$DISK_SIZE" "$IMG" sgdisk -o "$IMG" \ -n 1:2048:+64M -t 1:0700 -c 1:config \ @@ -65,38 +175,38 @@ sgdisk -o "$IMG" \ losetup -D LOOP=$(losetup --find --show -P "$IMG") - /sync-loop.sh "$LOOP" -TMP_LOOP="/tmp$LOOP" +TMP_LOOP="$FAKE_DEV/$(basename "$LOOP")" + mkfs.vfat -F 32 -n MONOK8S_CFG "${TMP_LOOP}p1" -mkfs.ext4 -F "${TMP_LOOP}p2" -mkfs.ext4 -F "${TMP_LOOP}p3" -mkfs.ext4 -F "${TMP_LOOP}p4" +mkfs.ext4 -F -L rootfsB "${TMP_LOOP}p3" +mkfs.ext4 -F -L data "${TMP_LOOP}p4" -mkdir -p /mnt/img-root /mnt/data +dd if="$ROOTFS_IMG" of="${TMP_LOOP}p2" bs=4M conv=fsync -mount "${TMP_LOOP}p2" /mnt/img-root -mount "${TMP_LOOP}p4" /mnt/data +# Grow each filesystem to fill its partition +e2fsck -fy "${TMP_LOOP}p2" +resize2fs "${TMP_LOOP}p2" -# Put the real /var onto the data partition -cp -a "$ROOTFS/var" /mnt/data/ -mkdir -p /mnt/data/etc-overlay/work -mkdir -p /mnt/data/etc-overlay/upper +# Populate data partition +mount "${TMP_LOOP}p4" "$MNT_DATA" -# Copy rootfs to root partition, but exclude /var -cp -a "$ROOTFS"/. /mnt/img-root/ -rm -rf /mnt/img-root/var -mkdir -p /mnt/img-root/var - -mkdir -p /mnt/img-root/boot -cp /build/board.itb /mnt/img-root/boot/kernel.itb +cp -a "$ROOTFS/var" "$MNT_DATA/" +mkdir -p "$MNT_DATA/etc-overlay/work" +mkdir -p "$MNT_DATA/etc-overlay/upper" sync -umount /mnt/img-root -umount /mnt/data +umount "$MNT_DATA" losetup -d "$LOOP" +cleanup_fake_nodes "$(basename "$LOOP")" +LOOP="" -echo "##################################################### Compressing Image" +echo "Built artifacts:" +echo " Full disk image: $IMG" +echo " Rootfs OTA image: $ROOTFS_IMG" +echo " Rootfs OTA compressed: $ROOTFS_IMG_ZST" + +echo "##################################################### Compressing Full Disk Image" gzip "/build/$IMG" diff --git a/alpine/sync-loop.sh b/alpine/sync-loop.sh index c7baeaa..0e8daa6 100755 --- a/alpine/sync-loop.sh +++ b/alpine/sync-loop.sh @@ -1,21 +1,29 @@ #!/bin/bash +set -euo pipefail + DEVICE="$1" FAKE_DEV="/tmp/dev" mkdir -p "$FAKE_DEV" +PARENT_NAME=$(basename "$DEVICE") -echo "Refreshing partition table..." -partx -u "$DEVICE" 2>/dev/null || partx -a "$DEVICE" +echo "Refreshing partition table for $DEVICE..." +partx -u "$DEVICE" 2>/dev/null || partx -a "$DEVICE" || true + +# Remove old fake nodes for this loop device first +find "$FAKE_DEV" -maxdepth 1 -type b -name "${PARENT_NAME}*" -exec rm -f {} \; -# Find partitions and their Major:Minor numbers lsblk -rn -o NAME,MAJ:MIN "$DEVICE" | while read -r NAME MAJMIN; do - # Skip the parent loop0 - if [[ "$NAME" == "loop0" ]]; then continue; fi + # Skip the parent loop device itself + if [[ "$NAME" == "$PARENT_NAME" ]]; then + continue + fi PART_PATH="$FAKE_DEV/$NAME" - MAJOR=$(echo $MAJMIN | cut -d: -f1) - MINOR=$(echo $MAJMIN | cut -d: -f2) + MAJOR="${MAJMIN%%:*}" + MINOR="${MAJMIN##*:}" echo "Creating node: $PART_PATH (b $MAJOR $MINOR)" + rm -f "$PART_PATH" mknod "$PART_PATH" b "$MAJOR" "$MINOR" done diff --git a/docker/alpine.Dockerfile b/docker/alpine.Dockerfile index 2cdb6a5..3ca42a2 100644 --- a/docker/alpine.Dockerfile +++ b/docker/alpine.Dockerfile @@ -1,7 +1,7 @@ -ARG TAG=dev +ARG BUILD_BASE_TAG=dev ARG DOCKER_IMAGE_ROOT=monok8s -FROM --platform=$BUILDPLATFORM ${DOCKER_IMAGE_ROOT}/build-base:${TAG} AS build-base +FROM --platform=$BUILDPLATFORM ${DOCKER_IMAGE_ROOT}/build-base:${BUILD_BASE_TAG} AS build-base ARG TAG ARG ALPINE_ARCH diff --git a/docker/build-base.Dockerfile b/docker/build-base.Dockerfile index 7d4e361..264e3e3 100644 --- a/docker/build-base.Dockerfile +++ b/docker/build-base.Dockerfile @@ -28,6 +28,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ pahole \ parted \ perl \ + pv \ python3 \ qemu-user-static \ podman \ @@ -36,6 +37,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ tar \ udev \ xz-utils \ + zstd \ dwarves \ gcc-aarch64-linux-gnu \ binutils-aarch64-linux-gnu \ diff --git a/makefile b/makefile index 04f8179..a797be8 100644 --- a/makefile +++ b/makefile @@ -39,6 +39,7 @@ BUILD_VERSION ?= $(KUBE_VERSION) BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) BUILD_GIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) BUILD_INFO_FILE := $(OUT_DIR)/build-info +BUILD_BASE_TAG := $(shell docker image inspect monok8s/build-base:dev | jq -r '.[].Id' | cut -d':' -f2 | cut -c -8 || echo dev) # ---- File groups ------------------------------------------------------------- @@ -148,6 +149,8 @@ $(BUILD_BASE_STAMP): $(BUILD_BASE_DEPS) | $(OUT_DIR) -f docker/build-base.Dockerfile \ --build-arg TAG=$(TAG) \ -t $(DOCKER_IMAGE_ROOT)/build-base:$(TAG) . + @iid=$$(docker image inspect monok8s/build-base:$(TAG) | jq -r '.[].Id' | cut -d':' -f2 | cut -c -8); \ + docker tag monok8s/build-base:$(TAG) monok8s/build-base:$$iid; \ touch $@ $(KERNEL_IMAGE): $(KERNEL_DEPS) | $(OUT_DIR) @@ -191,6 +194,7 @@ $(BOARD_ITB): $(ITB_DEPS) | $(OUT_DIR) $(RELEASE_IMAGE): $(RELEASE_DEPS) | $(OUT_DIR) docker build \ -f docker/alpine.Dockerfile \ + --no-cache \ --build-arg DOCKER_IMAGE_ROOT=$(DOCKER_IMAGE_ROOT) \ --build-arg TAG=$(TAG) \ --build-arg ALPINE_ARCH=$(ALPINE_ARCH) \ @@ -198,6 +202,7 @@ $(RELEASE_IMAGE): $(RELEASE_DEPS) | $(OUT_DIR) --build-arg KUBE_VERSION=$(KUBE_VERSION) \ --build-arg CRIO_VERSION=$(CRIO_VERSION) \ --build-arg DEVICE_TREE_TARGET=$(DEVICE_TREE_TARGET) \ + --build-arg BUILD_BASE_TAG=$(BUILD_BASE_TAG) \ -t $(DOCKER_IMAGE_ROOT)/buildenv-alpine:$(TAG) . @cid=$$(docker create \ @@ -219,6 +224,7 @@ $(RELEASE_IMAGE): $(RELEASE_DEPS) | $(OUT_DIR) bash -lc '/build-rootfs.sh'); \ docker start -a $$cid; \ docker cp $$cid:/build/output.img.gz $@; \ + docker cp $$cid:/build/rootfs.ext4.zst $(OUT_DIR)/rootfs.ext4.zst; \ docker rm $$cid test -f $@