From 21ad64abbc3690fa9ac9789d72921926df81fc7a Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Wed, 27 May 2026 18:14:10 +0100 Subject: [PATCH 1/3] Add Docker-based build environment The CirrOS build system has historically required a bare Ubuntu 22.04 host with a specific set of system packages installed via system-setup. This makes it difficult to build CirrOS reproducibly on modern workstations or in CI environments running a different host OS. This change introduces a Docker-based build environment that encapsulates all build dependencies in an ubuntu:22.04 image while preserving the existing native build-release contract. Dockerfile installs all packages from bin/system-setup plus the implicit dependencies (binutils, cpio, kmod, sudo) and the CI extras (qemu-system-*, cloud-utils, openbios-ppc) in a single layer. It also sets USER=root so the existing `sudo chown -R $USER:$USER` ownership handoff remains valid when bin/build-release runs inside the container. It pre-creates /dev/loop0-7 nodes and declares /cirros/download and /cirros/ccache as volumes so that build caches persist across runs. docker-entrypoint.sh validates that the cirros source tree is bind-mounted at /cirros, loads the loop kernel module (required for bin/bundle's loopback mounts), and delegates to bin/build-release. The ARCHES environment variable can be set to limit the build to a subset of architectures, e.g. ARCHES=x86_64. bin/build-release also updates the Buildroot download URL from the defunct buildroot.uclibc.org domain to buildroot.org, which now hosts the official release archives. The existing sudo-based bundle and chown steps are intentionally left unchanged so native and GitHub Actions builds keep working as before. To build an x86_64 image: docker build -t cirros-builder . docker run --privileged --rm \ -e ARCHES=x86_64 \ -v "$PWD":/cirros \ -v cirros-dl:/cirros/download \ -v cirros-ccache:/cirros/ccache \ cirros-builder daily The --privileged flag is required because bin/bundle mounts a loopback partition image during the disk image assembly stage. Assisted-By: pi claude-sonnet-4-6 Signed-off-by: Sean Mooney --- Dockerfile | 94 ++++++++++++++++++++++++++++++++++++++++++++ bin/build-release | 2 +- docker-entrypoint.sh | 25 ++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100755 docker-entrypoint.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7ecb001 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,94 @@ +# Dockerfile for CirrOS build environment +# +# Build image: +# docker build -t cirros-builder . +# +# Run (--privileged is required for loop mounts inside bin/bundle): +# docker run --privileged --rm \ +# -v "$PWD":/cirros \ +# -v cirros-dl:/cirros/download \ +# -v cirros-ccache:/cirros/ccache \ +# cirros-builder daily +# +# Build a specific tagged release: +# docker run --privileged --rm \ +# -v "$PWD":/cirros \ +# -v cirros-dl:/cirros/download \ +# -v cirros-ccache:/cirros/ccache \ +# cirros-builder 0.3.2 +# +# Build only x86_64: +# docker run --privileged --rm \ +# -e ARCHES=x86_64 \ +# -v "$PWD":/cirros \ +# -v cirros-dl:/cirros/download \ +# -v cirros-ccache:/cirros/ccache \ +# cirros-builder daily + +FROM ubuntu:22.04 + +LABEL description="CirrOS image build environment (Ubuntu 22.04 / Buildroot 2022.02.4)" + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=UTC +ENV USER=root + +# ── All build dependencies ──────────────────────────────────────────────────── +# system-setup DEPS + CI extras + tools used implicitly (cpio, kmod, binutils, sudo) +RUN apt-get update && apt-get install -y --no-install-recommends \ + # system-setup DEPS + bc \ + bison \ + build-essential \ + dosfstools \ + flex \ + gdisk \ + gettext \ + git \ + grub-common \ + kpartx \ + libncurses5-dev \ + lsb-release \ + mtools \ + parallel \ + python3 \ + qemu-utils \ + quilt \ + rsync \ + sudo \ + texinfo \ + unzip \ + wget \ + xz-utils \ + zstd \ + # implicit deps (used by bundle/grab-kernels but not listed in system-setup) + binutils \ + cpio \ + kmod \ + # CI extras (boot testing + OpenFirmware for ppc64le) + cloud-utils \ + openbios-ppc \ + qemu-system-arm \ + qemu-system-misc \ + qemu-system-ppc \ + qemu-system-x86 \ + && rm -rf /var/lib/apt/lists/* + +# ── Loop device nodes ───────────────────────────────────────────────────────── +# Pre-create /dev/loop* nodes for environments where the container /dev is +# minimal. At runtime --privileged exposes the host kernel's loop subsystem. +RUN for i in $(seq 0 7); do \ + [ -e /dev/loop$i ] || mknod /dev/loop$i b 7 $i; \ + done + +WORKDIR /cirros + +# Entrypoint that validates the bind-mount and delegates to bin/build-release +COPY docker-entrypoint.sh /usr/local/bin/cirros-build +RUN chmod +x /usr/local/bin/cirros-build + +# Declare cache-friendly volume mount points +VOLUME ["/cirros/download", "/cirros/ccache"] + +ENTRYPOINT ["/usr/local/bin/cirros-build"] +CMD ["daily"] diff --git a/bin/build-release b/bin/build-release index 7d2f539..66b2dae 100755 --- a/bin/build-release +++ b/bin/build-release @@ -166,7 +166,7 @@ tstart=${_RET} # Stage 1: DOWNLOAD logevent "start download" - brtgz="buildroot-${BR_VER}.tar.gz" -dl "http://buildroot.uclibc.org/downloads/$brtgz" "download/$brtgz" +dl "https://buildroot.org/downloads/$brtgz" "download/$brtgz" logevent "end download" logevent "start unpack" - diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..5f6d3b0 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# docker-entrypoint.sh — run inside the cirros-builder container +set -euo pipefail + +# Ensure loop device support (requires --privileged) +modprobe loop max_loop=16 2>/dev/null || true + +# Expect the cirros source tree to be bind-mounted at /cirros +cd /cirros +[ -f bin/build-release ] || { + echo "ERROR: /cirros does not look like a cirros source tree." + echo " Bind-mount the repo: -v \$PWD:/cirros" + exit 1 +} + +VERSION="${1:-daily}" +shift || true + +export CI_BUILD="${CI_BUILD:-true}" +export CI_BUILD_OUT="${CI_BUILD_OUT:-build-ci/}" + +echo "==> Building CirrOS version: ${VERSION}" +echo "==> Architectures: ${ARCHES:-x86_64 arm aarch64 ppc64le riscv64}" + +exec bin/build-release "${VERSION}" "$@" From d7babb1daee265a7e117ab5b201b2f274a4879e0 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Wed, 27 May 2026 18:33:50 +0100 Subject: [PATCH 2/3] Add pre-populated qcow2 image variant CirrOS currently ships a bootable qcow2 image with a mostly empty root filesystem partition. On first boot the initramfs detects that the labeled rootfs partition does not contain /sbin/init, mounts it read-write, and copies the initramfs contents into the partition before switching root. If that first boot is interrupted, the partition can be left only partly populated. A later boot may then find /sbin/init and switch into the partition, but fail when a required shared library or file was never copied. That presents as PID 1 exiting, followed by a kernel panic. Keep the traditional minimal qcow2 image for compatibility, but rename it from the old .img suffix to -minimal.qcow2 so its format is explicit. Add a second -full.qcow2 image whose root filesystem is pre-populated at build time from the assembled filesys tarball. The full initramfs is still copied into /boot so that the early boot environment has the kernel modules required to find and mount the root device. This removes the first-boot rootfs copy window for users that choose the full image while preserving the existing minimal image and initramfs fallback behavior. Assisted-By: pi claude-sonnet-4-6 Signed-off-by: Sean Mooney --- README.md | 5 ++- bin/build-release | 16 +++++---- bin/bundle | 67 ++++++++++++++++++++++++------------ bin/mirror-dump-sstream-data | 2 +- bin/test-boot | 7 ++-- 5 files changed, 65 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index e28fb28..220b955 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ the ARCHES variable like: * ARCHES=aarch64,x86_64,arm bin/build-release daily Resulting images will be present in ../build-dYYMMDD/release directory. +Each architecture gets a bootable `*-minimal.qcow2` image with the traditional +minimal root filesystem and a `*-full.qcow2` image with the root filesystem +pre-populated at build time. ## Long, detailed version @@ -128,7 +131,7 @@ for riscv64,use below version We provide simple script to test resulting image. You run it this way: ```bash - $ RELEASE_DIR=$PWD/../build-*/release IMG=$PWD/../build-*/release/cirros-*-x86_64-disk.img bin/test-boot + $ RELEASE_DIR=$PWD/../build-*/release IMG=$PWD/../build-*/release/cirros-*-x86_64-full.qcow2 bin/test-boot ``` Note: "RELEASE_DIR" variable is required only for aarch64 and arm images. diff --git a/bin/build-release b/bin/build-release index 66b2dae..8ff3286 100755 --- a/bin/build-release +++ b/bin/build-release @@ -284,7 +284,8 @@ for arch in ${ARCHES}; do ln initramfs $p-initrd && ln initramfs $p-initramfs && ln part.img $p-rootfs.img && ln blank.img $p-blank.img && - ln disk.img $p-disk.img && + ln disk-minimal.qcow2 $p-minimal.qcow2 && + ln disk-full.qcow2 $p-full.qcow2 && ln filesys.tar.gz $p-lxc.tar.gz && ln filesys.tar.xz $p-lxc.tar.xz && ln lxd.tar.xz $p-lxd.tar.xz && @@ -302,7 +303,8 @@ for arch in ${ARCHES}; do ( cd "$OUT/stage/$arch" && tar cvzf - $p-blank.img $p-vmlinuz $p-initrd ) > "$OUT/release/$p-uec.tar.gz" - cp "$OUT/stage/$arch/$p-disk.img" "$OUT/release/$p-disk.img" + cp "$OUT/stage/$arch/$p-minimal.qcow2" "$OUT/release/$p-minimal.qcow2" + cp "$OUT/stage/$arch/$p-full.qcow2" "$OUT/release/$p-full.qcow2" done mkdir -p "$OUT/release/buildroot_rootfs" @@ -323,10 +325,12 @@ msg "output in $OUT/release" if [ "true" = "${BOOTTEST}" ]; then for arch in ${ARCHES}; do - img="$OUT/release/cirros-$VER-$arch-disk.img" - logevent "start test-boot $arch ${img##*/}" - - POWEROFF=true IMG="$img" RELEASE_DIR=$OUT/release test-boot - logevent "end test-boot $arch ${img##*/}" + for variant in minimal full; do + img="$OUT/release/cirros-$VER-$arch-$variant.qcow2" + logevent "start test-boot $arch ${img##*/}" - + POWEROFF=true IMG="$img" RELEASE_DIR=$OUT/release test-boot + logevent "end test-boot $arch ${img##*/}" + done done fi diff --git a/bin/bundle b/bin/bundle index 363bffa..1848b3f 100755 --- a/bin/bundle +++ b/bin/bundle @@ -99,10 +99,10 @@ mkdir -p "${out_d_in}" && out_d=$(readlink -f "${out_d_in}") && fail "failed to get full path for input" out_partimg="${out_d}/part.img" -out_diskimg="${out_d}/disk.img" +out_minimal_diskimg="${out_d}/disk-minimal.qcow2" +out_full_diskimg="${out_d}/disk-full.qcow2" out_kernel="${out_d}/kernel" out_initramfs="${out_d}/initramfs" -out_diskimg="${out_d}/disk.img" out_blankimg="${out_d}/blank.img" out_filesys_lxc_gz="${out_d}/filesys.tar.gz" out_filesys_lxc_xz="${out_d}/filesys.tar.xz" @@ -322,33 +322,38 @@ tar -C "$stage_d" -cpf - \ ( cd "$stage_d" && tar -cpf - * ) > "$filesys_tar" || fail "failed to create filesys_tar" -debug 1 "populating image" +debug 1 "populating minimal partition image" mount -o loop "${out_partimg}" "${mp}" && UMOUNT=${mp} || fail "failed to mount ${out_partimg} loopback" -tar -C "$mp" -xpf - vmlinuz initrd.img boot/ < "$kernel_tar" || +tar -C "$mp" -xpf - < "$kernel_tar" || fail "failed to extract kernel_tar" cp "$initramfs" "$mp/boot/initrd.img-${kver}" umount "${mp}" && UMOUNT="" || fail "failed to unmount ${out_partimg}" +full_partimg="$TEMP_D/part.img.full" +cp "$out_blankimg" "$full_partimg" || + fail "failed to copy full partition image" + +debug 1 "populating full partition image" +mount -o loop "${full_partimg}" "${mp}" && UMOUNT=${mp} || + fail "failed to mount ${full_partimg} loopback" + +tar -C "$mp" -xpf - < "$filesys_tar" || + fail "failed to extract filesys_tar" +cp "$initramfs" "$mp/boot/initrd.img-${kver}" + +umount "${mp}" && UMOUNT="" || + fail "failed to unmount ${full_partimg}" + cp "${kernel_d}/${vmlinuz}" "${out_kernel}" || fail "failed to copy kernel to ${out_kernel}" { [ -z "${out_initramfs}" ] || cp "${initramfs}" "${out_initramfs}"; } || fail "failed to copy initramfs to ${out_initramfs}" -debug 1 "fixing grub entry in partimg" -tmp_part="$TEMP_D/part.img.disk" -cp "$out_partimg" "$tmp_part" && - mount -o loop "$tmp_part" "$mp" && UMOUNT="$mp" || - fail "failed to mount $tmp_part" -sed -i 's/(hd0)/(hd0,0)/' "$mp/boot/grub/menu.lst" || - fail "failed to edit /boot/grub/menu.lst in image" -umount "$mp" && UMOUNT="" || - fail "failed to unmount partimg" - case $arch in x86_64) grub_options="--grub1 --grub-efi";; arm|aarch64|riscv64) grub_options="--grub-efi";; @@ -356,13 +361,30 @@ case $arch in *) grub_options=;; esac -debug 1 "creating disk image" -out=$(PATH=$xgrubd:$PATH part2disk $grub_options "$tmp_part" \ - "$grub_in" "$arch" "$out_diskimg.raw" 2>&1) || - fail "failed to create disk image: $out" -qemu-img convert -O qcow2 -c "$out_diskimg.raw" "$out_diskimg" || - fail "failed to convert disk image" -rm -f "$out_diskimg.raw" "$tmp_part" +write_disk_image() { + local partimg="$1" out_diskimg="$2" tmp_part="" + + debug 1 "fixing grub entry in ${partimg}" + tmp_part="$TEMP_D/${out_diskimg##*/}.part" + cp "$partimg" "$tmp_part" && + mount -o loop "$tmp_part" "$mp" && UMOUNT="$mp" || + fail "failed to mount $tmp_part" + sed -i 's/(hd0)/(hd0,0)/' "$mp/boot/grub/menu.lst" || + fail "failed to edit /boot/grub/menu.lst in image" + umount "$mp" && UMOUNT="" || + fail "failed to unmount partimg" + + debug 1 "creating disk image ${out_diskimg}" + out=$(PATH=$xgrubd:$PATH part2disk $grub_options "$tmp_part" \ + "$grub_in" "$arch" "$out_diskimg.raw" 2>&1) || + fail "failed to create disk image: $out" + qemu-img convert -O qcow2 -c "$out_diskimg.raw" "$out_diskimg" || + fail "failed to convert disk image" + rm -f "$out_diskimg.raw" "$tmp_part" +} + +write_disk_image "$out_partimg" "$out_minimal_diskimg" +write_disk_image "$full_partimg" "$out_full_diskimg" if [ -n "${SUDO_USER}" ]; then u=${SUDO_USER} @@ -372,7 +394,8 @@ if [ -n "${SUDO_USER}" ]; then fi echo "wrote ${out_partimg}" -echo "wrote ${out_diskimg}" +echo "wrote ${out_minimal_diskimg}" +echo "wrote ${out_full_diskimg}" echo "wrote ${out_kernel}" echo "wrote ${out_initramfs}" echo "wrote ${out_blankimg}" diff --git a/bin/mirror-dump-sstream-data b/bin/mirror-dump-sstream-data index 5e2690d..c53d373 100755 --- a/bin/mirror-dump-sstream-data +++ b/bin/mirror-dump-sstream-data @@ -127,7 +127,7 @@ output_for_path() { local path=${1#./} local size="" md5="" sha256="" pubname="" case "$path" in - *-uec.tar.gz|*-disk.img|*-rootfs.img|*-lxc.tar.gz|*-lxc.tar.xz) :;; + *-uec.tar.gz|*-minimal.qcow2|*-full.qcow2|*-rootfs.img|*-lxc.tar.gz|*-lxc.tar.xz) :;; *-lxd.tar.xz) :;; *) return;; esac diff --git a/bin/test-boot b/bin/test-boot index 4b0a235..be48785 100755 --- a/bin/test-boot +++ b/bin/test-boot @@ -24,12 +24,15 @@ POWEROFF=${POWEROFF:-false} if [ "$CI_BUILD" = "true" ]; then GUESTARCH="${GUESTARCH:-}" - IMG=$(find $CI_BUILD_OUT/release/ -name cirros-*-${GUESTARCH}-disk.img) - RELEASE_DIR=$CI_BUILD_OUT/release + RELEASE_DIR=${RELEASE_DIR:-$CI_BUILD_OUT/release} + if [ -z "${IMG:-}" ]; then + IMG=$(find "$RELEASE_DIR" -name "cirros-*-${GUESTARCH}-full.qcow2" | head -n1) + fi else GUESTARCH="${GUESTARCH:-$(echo $(basename $IMG)| cut -d'-' -f3)}" fi +GUESTARCH="${GUESTARCH:-$(echo $(basename $IMG)| cut -d'-' -f3)}" VER=$(echo $(basename $IMG)| cut -d'-' -f2) echo '{"instance-id": "9068aef2-213e-4e43-830f-accdbadde897"}' > meta-data From 6ab7a3ff74507afcd1dd95fba657f7c38971cc4a Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Wed, 27 May 2026 18:36:56 +0100 Subject: [PATCH 3/3] Use ext4 for root filesystem images CirrOS root filesystem partition images are currently formatted as ext3. The previous change makes the partition hold the real root filesystem at build time, so use ext4 for that filesystem to get the newer ext4 layout and metadata handling while keeping compatibility with the existing resize2fs-based grow path. The initramfs mounts the root device by label without forcing a filesystem type, so no initramfs change is needed. The generated x86_64 image was also boot-tested under QEMU to confirm GRUB can load the kernel and initramfs from the ext4 partition and the system reaches the CirrOS login prompt. Tested with an x86_64 Docker build and a QEMU boot smoke test. The image boots to the CirrOS login prompt, mounts /dev/vda1 as the root filesystem, and does not copy the initramfs to the rootfs partition at boot. Assisted-By: pi claude-sonnet-4-6 Signed-off-by: Sean Mooney --- bin/bundle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/bundle b/bin/bundle index 1848b3f..2f69e45 100755 --- a/bin/bundle +++ b/bin/bundle @@ -55,7 +55,7 @@ getopt_out=$(getopt --name "${0##*/}" \ topdir=$(cd "${0%/*}/.." && pwd) size=${DEF_SIZE} FS_LABEL="cirros-rootfs" -fs_type="ext3" +fs_type="ext4" arch="" while [ $# -ne 0 ]; do