From a166068fabc77855be4fb7343532fcb1b7929eb4 Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Sat, 21 Mar 2026 20:25:55 -0300 Subject: [PATCH] Add an experimental Alpine image flow Stage a complete Alpine x86_64 image stack so \ --image alpineworks like the existing manual Void path instead of relying on Debian-oriented image builds.\n\nAdd make targets plus kernel/rootfs/register helpers that download pinned Alpine artifacts, extract a Firecracker-compatible vmlinux, build a matching mkinitfs initramfs, seed OpenRC services, and register/promote a managed image named alpine.\n\nFold in the bring-up fixes discovered during boot validation: use rootfstype=ext4 in shared boot args, install libgcc/libstdc++ for the opencode binary, and give opencode more time to become ready on cold boots.\n\nValidate with go test ./..., the Alpine helper builds, image promotion, and banger vm create --image alpine --name alp --nat plus guest service and port checks. --- Makefile | 27 +- README.md | 40 ++ examples/alpine.config.toml | 9 + internal/cli/banger.go | 6 +- internal/cli/cli_test.go | 18 + internal/imagepreset/preset.go | 29 ++ internal/opencode/opencode.go | 2 +- internal/system/system.go | 2 +- internal/system/system_test.go | 2 +- scripts/customize.sh | 2 +- scripts/interactive.sh | 2 +- scripts/make-alpine-kernel.sh | 363 ++++++++++++++++ scripts/make-rootfs-alpine.sh | 722 +++++++++++++++++++++++++++++++ scripts/register-alpine-image.sh | 92 ++++ 14 files changed, 1307 insertions(+), 9 deletions(-) create mode 100644 examples/alpine.config.toml create mode 100755 scripts/make-alpine-kernel.sh create mode 100755 scripts/make-rootfs-alpine.sh create mode 100755 scripts/register-alpine-image.sh diff --git a/Makefile b/Makefile index 1b4e37e..4dd0db6 100644 --- a/Makefile +++ b/Makefile @@ -17,10 +17,13 @@ BINARIES := $(BANGER_BIN) $(BANGERD_BIN) $(VSOCK_AGENT_BIN) GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort) VOID_IMAGE_NAME ?= void-exp VOID_VM_NAME ?= void-dev +ALPINE_RELEASE ?= 3.23.3 +ALPINE_IMAGE_NAME ?= alpine +ALPINE_VM_NAME ?= alpine-dev .DEFAULT_GOAL := help -.PHONY: help build banger bangerd test fmt tidy clean rootfs rootfs-void void-kernel void-register void-vm verify-void install bench-create +.PHONY: help build banger bangerd test fmt tidy clean rootfs rootfs-void void-kernel void-register void-vm verify-void alpine-kernel rootfs-alpine alpine-register alpine-vm verify-alpine install bench-create help: @printf '%s\n' \ @@ -37,7 +40,12 @@ help: ' make rootfs-void Build an experimental Void Linux rootfs and work-seed in ./build/manual' \ ' make void-register Register or update the experimental Void image as $(VOID_IMAGE_NAME)' \ ' make void-vm Register the experimental Void image and create a VM named $(VOID_VM_NAME)' \ - ' make verify-void Register the experimental Void image and run scripts/verify.sh against it' + ' make verify-void Register the experimental Void image and run scripts/verify.sh against it' \ + ' make alpine-kernel Download and stage an Alpine virt kernel, initramfs, and modules under ./build/manual/alpine-kernel' \ + ' make rootfs-alpine Build an experimental Alpine Linux rootfs and work-seed in ./build/manual' \ + ' make alpine-register Register or update the experimental Alpine image as $(ALPINE_IMAGE_NAME)' \ + ' make alpine-vm Register the experimental Alpine image and create a VM named $(ALPINE_VM_NAME)' \ + ' make verify-alpine Register the experimental Alpine image and run scripts/verify.sh against it' build: $(BINARIES) @@ -92,3 +100,18 @@ void-vm: void-register verify-void: void-register BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/verify.sh --image "$(VOID_IMAGE_NAME)" + +alpine-kernel: + BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" ALPINE_RELEASE="$(ALPINE_RELEASE)" ./scripts/make-alpine-kernel.sh $(ARGS) + +rootfs-alpine: + BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" ALPINE_RELEASE="$(ALPINE_RELEASE)" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/make-rootfs-alpine.sh $(ARGS) + +alpine-register: build + BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" ALPINE_IMAGE_NAME="$(ALPINE_IMAGE_NAME)" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/register-alpine-image.sh + +alpine-vm: alpine-register + "$(abspath $(BANGER_BIN))" vm create --image "$(ALPINE_IMAGE_NAME)" --name "$(ALPINE_VM_NAME)" + +verify-alpine: alpine-register + BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/verify.sh --image "$(ALPINE_IMAGE_NAME)" diff --git a/README.md b/README.md index 88f8914..869f814 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,46 @@ That flow uses: - `./build/manual/rootfs-void.ext4` - `./build/manual/rootfs-void.work-seed.ext4` +## Experimental Alpine Flow + +Stage an Alpine virt kernel: + +```bash +make alpine-kernel +``` + +Build the experimental Alpine rootfs: + +```bash +make rootfs-alpine +``` + +Register it: + +```bash +make alpine-register +``` + +Create a VM from it: + +```bash +./build/bin/banger vm create --image alpine --name alpine-dev +``` + +That flow uses: + +- `./build/manual/alpine-kernel/` +- `./build/manual/rootfs-alpine.ext4` +- `./build/manual/rootfs-alpine.work-seed.ext4` + +The experimental Alpine flow stages a pinned Alpine release by default. Override +that pin with `ALPINE_RELEASE=...` when running the `make alpine-kernel` and +`make rootfs-alpine` helpers if you need a different patch release. + +Alpine support currently applies to the explicit register-and-run flow above. +The generic `banger image build --from-image ...` path remains Debian/systemd- +oriented and should not be treated as an Alpine image builder. + ## Notes - Firecracker is resolved from `PATH` by default. diff --git a/examples/alpine.config.toml b/examples/alpine.config.toml new file mode 100644 index 0000000..c4e1011 --- /dev/null +++ b/examples/alpine.config.toml @@ -0,0 +1,9 @@ +# Experimental Alpine Linux guest profile for local testing. +# +# Register or promote a complete `alpine` image first, then point the daemon +# at it by name. Firecracker is resolved from PATH by default; set +# `firecracker_bin` only if you need an override. + +default_image_name = "alpine" +# firecracker_bin = "/usr/bin/firecracker" +# ssh_key_path = "/abs/path/to/private/key" diff --git a/internal/cli/banger.go b/internal/cli/banger.go index 761172b..f0ff81f 100644 --- a/internal/cli/banger.go +++ b/internal/cli/banger.go @@ -175,9 +175,9 @@ func newInternalVSockAgentPathCommand() *cobra.Command { func newInternalPackagesCommand() *cobra.Command { var docker bool cmd := &cobra.Command{ - Use: "packages ", + Use: "packages ", Hidden: true, - Args: exactArgsUsage(1, "usage: banger internal packages [--docker]"), + Args: exactArgsUsage(1, "usage: banger internal packages [--docker]"), RunE: func(cmd *cobra.Command, args []string) error { var packages []string switch strings.TrimSpace(args[0]) { @@ -188,6 +188,8 @@ func newInternalPackagesCommand() *cobra.Command { } case "void": packages = imagepreset.VoidBasePackages() + case "alpine": + packages = imagepreset.AlpineBasePackages() default: return fmt.Errorf("unknown package preset %q", args[0]) } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index f82b76d..6482353 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -111,6 +111,24 @@ func TestInternalNATFlagsExist(t *testing.T) { } } +func TestInternalPackagesCommandSupportsAlpine(t *testing.T) { + cmd := NewBangerCommand() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"internal", "packages", "alpine"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute(): %v", err) + } + + output := stdout.String() + for _, want := range []string{"alpine-base", "docker", "libgcc", "libstdc++", "mkinitfs", "openssh"} { + if !strings.Contains(output, want+"\n") { + t.Fatalf("output = %q, want package %q", output, want) + } + } +} + func TestVMCreateFlagsExist(t *testing.T) { root := NewBangerCommand() vm, _, err := root.Find([]string{"vm"}) diff --git a/internal/imagepreset/preset.go b/internal/imagepreset/preset.go index d1de16e..3f60ba7 100644 --- a/internal/imagepreset/preset.go +++ b/internal/imagepreset/preset.go @@ -43,6 +43,31 @@ var voidBase = []string{ "wget", } +var alpineBase = []string{ + "alpine-base", + "bash", + "ca-certificates", + "curl", + "docker", + "docker-cli-compose", + "e2fsprogs", + "git", + "iproute2", + "less", + "libgcc", + "libstdc++", + "make", + "mkinitfs", + "openssh", + "procps-ng", + "shadow", + "sudo", + "tmux", + "tree", + "vim", + "wget", +} + func DebianBasePackages() []string { return append([]string(nil), debianBase...) } @@ -51,6 +76,10 @@ func VoidBasePackages() []string { return append([]string(nil), voidBase...) } +func AlpineBasePackages() []string { + return append([]string(nil), alpineBase...) +} + func Hash(lines []string) string { sum := sha256.Sum256([]byte(strings.Join(lines, "\n") + "\n")) return fmt.Sprintf("%x", sum) diff --git a/internal/opencode/opencode.go b/internal/opencode/opencode.go index e5d75c6..01d989f 100644 --- a/internal/opencode/opencode.go +++ b/internal/opencode/opencode.go @@ -17,7 +17,7 @@ const ( ShimPath = "/root/.local/share/mise/shims/opencode" ServiceName = "banger-opencode.service" RunitServiceName = "banger-opencode" - ReadyTimeout = 15 * time.Second + ReadyTimeout = 45 * time.Second pollInterval = 200 * time.Millisecond ) diff --git a/internal/system/system.go b/internal/system/system.go index fc81fd5..0559d8b 100644 --- a/internal/system/system.go +++ b/internal/system/system.go @@ -417,7 +417,7 @@ func UpdateFSTab(existing string) string { func BuildBootArgs(vmName, guestIP, bridgeIP, dns string) string { return fmt.Sprintf( - "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=%s::%s:255.255.255.0:%s:eth0:off:%s hostname=%s systemd.mask=home.mount systemd.mask=var.mount", + "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rootfstype=ext4 rw ip=%s::%s:255.255.255.0:%s:eth0:off:%s hostname=%s systemd.mask=home.mount systemd.mask=var.mount", guestIP, bridgeIP, vmName, diff --git a/internal/system/system_test.go b/internal/system/system_test.go index c72eca5..4da8ba7 100644 --- a/internal/system/system_test.go +++ b/internal/system/system_test.go @@ -171,7 +171,7 @@ func TestBuildBootArgsIncludesHostnameInIPField(t *testing.T) { t.Parallel() got := BuildBootArgs("devbox", "172.16.0.2", "172.16.0.1", "1.1.1.1") - want := "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=172.16.0.2::172.16.0.1:255.255.255.0:devbox:eth0:off:1.1.1.1 hostname=devbox systemd.mask=home.mount systemd.mask=var.mount" + want := "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rootfstype=ext4 rw ip=172.16.0.2::172.16.0.1:255.255.255.0:devbox:eth0:off:1.1.1.1 hostname=devbox systemd.mask=home.mount systemd.mask=var.mount" if got != want { t.Fatalf("BuildBootArgs() = %q, want %q", got, want) } diff --git a/scripts/customize.sh b/scripts/customize.sh index 608f06a..eacc51e 100755 --- a/scripts/customize.sh +++ b/scripts/customize.sh @@ -303,7 +303,7 @@ sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \ "smt": false }' >/dev/null -KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount" +KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rootfstype=ext4 rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount" INITRD_JSON="" if [[ -n "$INITRD" ]]; then diff --git a/scripts/interactive.sh b/scripts/interactive.sh index e579faf..deb262b 100755 --- a/scripts/interactive.sh +++ b/scripts/interactive.sh @@ -230,7 +230,7 @@ sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \ "smt": false }' >/dev/null -KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount" +KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rootfstype=ext4 rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount" sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \ -H "Content-Type: application/json" \ diff --git a/scripts/make-alpine-kernel.sh b/scripts/make-alpine-kernel.sh new file mode 100755 index 0000000..8bcf2fe --- /dev/null +++ b/scripts/make-alpine-kernel.sh @@ -0,0 +1,363 @@ +#!/usr/bin/env bash +set -euo pipefail + +log() { + printf '[make-alpine-kernel] %s\n' "$*" +} + +usage() { + cat <<'EOF' +Usage: ./scripts/make-alpine-kernel.sh [--out-dir ] [--release ] [--mirror ] [--arch ] [--print-register-flags] + +Download and stage an Alpine Linux virt kernel under ./build/manual/alpine-kernel +for the experimental Alpine guest flow. + +Defaults: + --out-dir ./build/manual/alpine-kernel + --release 3.23.3 + --mirror https://dl-cdn.alpinelinux.org/alpine + --arch x86_64 + +The staged output contains: + boot/vmlinuz- Alpine virt kernel image + boot/initramfs-.img Matching Alpine initramfs + boot/config- Alpine kernel config when present + lib/modules// Matching kernel modules from modloop-virt + +If --print-register-flags is passed, the script does not download anything. It +prints the banger image register flags for an existing staged Alpine kernel. +EOF +} + +require_command() { + local name="$1" + command -v "$name" >/dev/null 2>&1 || { + log "required command not found: $name" + exit 1 + } +} + +check_elf() { + local path="$1" + readelf -h "$path" >/dev/null 2>&1 +} + +find_latest_matching() { + local dir="$1" + local pattern="$2" + if [[ ! -d "$dir" ]]; then + return 1 + fi + find "$dir" -maxdepth 1 -type f -name "$pattern" | sort | tail -n 1 +} + +find_latest_module_dir() { + local root="$1" + local dir="" + if [[ ! -d "$root" ]]; then + return 1 + fi + while IFS= read -r dir; do + if [[ -d "$dir/kernel" || -f "$dir/modules.dep" || -f "$dir/modules.dep.bin" ]]; then + printf '%s\n' "$dir" + return 0 + fi + done < <(find "$root" -mindepth 1 -maxdepth 1 -type d | sort) + return 1 +} + +find_tar_entry() { + local archive="$1" + local needle="$2" + local entry="" + + while IFS= read -r entry; do + case "$entry" in + "$needle"|*/"$needle") + printf '%s\n' "$entry" + return 0 + ;; + esac + done < <(tar -tf "$archive") + + return 1 +} + +find_tar_config_entry() { + local archive="$1" + local entry="" + + while IFS= read -r entry; do + case "$entry" in + config-*-virt|*/config-*-virt) + printf '%s\n' "$entry" + return 0 + ;; + esac + done < <(tar -tf "$archive") + + return 1 +} + +resolve_release_branch() { + local release="$1" + printf 'v%s\n' "${release%.*}" +} + +extract_vmlinux() { + local image="$1" + local out="$2" + local tmp="$TMP_DIR/vmlinux.extract" + + if check_elf "$image"; then + install -m 0644 "$image" "$out" + return 0 + fi + + try_decompress() { + local header="$1" + local marker="$2" + local command="$3" + local pos="" + + while IFS= read -r pos; do + [[ -n "$pos" ]] || continue + pos="${pos%%:*}" + tail -c+"$pos" "$image" | eval "$command" >"$tmp" 2>/dev/null || true + if check_elf "$tmp"; then + install -m 0644 "$tmp" "$out" + return 0 + fi + done < <(tr "$header\n$marker" "\n$marker=" < "$image" | grep -abo "^$marker" || true) + + return 1 + } + + try_decompress '\037\213\010' "xy" "gunzip" && return 0 + try_decompress '\3757zXZ\000' "abcde" "unxz" && return 0 + try_decompress "BZh" "xy" "bunzip2" && return 0 + try_decompress '\135\000\000\000' "xxx" "unlzma" && return 0 + try_decompress '\002!L\030' "xxx" "lz4 -d" && return 0 + try_decompress '(\265/\375' "xxx" "unzstd" && return 0 + + return 1 +} + +print_register_flags() { + local kernel="" + local initrd="" + local modules="" + + kernel="$(find_latest_matching "$OUT_DIR/boot" 'vmlinux-*' || true)" + if [[ -z "$kernel" ]]; then + kernel="$(find_latest_matching "$OUT_DIR/boot" 'vmlinuz-*' || true)" + fi + initrd="$(find_latest_matching "$OUT_DIR/boot" 'initramfs-*' || true)" + modules="$(find_latest_module_dir "$OUT_DIR/lib/modules" || true)" + + if [[ -z "$kernel" || -z "$modules" ]]; then + log "staged Alpine kernel not found under $OUT_DIR" + exit 1 + fi + + printf -- '--kernel %q ' "$kernel" + if [[ -n "$initrd" ]]; then + printf -- '--initrd %q ' "$initrd" + fi + printf -- '--modules %q\n' "$modules" +} + +cleanup() { + if [[ "${MODLOOP_MOUNTED:-0}" == "1" ]] && [[ -n "${MODLOOP_MOUNT:-}" ]]; then + sudo umount "$MODLOOP_MOUNT" || true + fi + if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then + rm -rf "$TMP_DIR" + fi +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +MANUAL_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}" +OUT_DIR="$MANUAL_DIR/alpine-kernel" +RELEASE="${ALPINE_RELEASE:-3.23.3}" +MIRROR="https://dl-cdn.alpinelinux.org/alpine" +ARCH="x86_64" +PRINT_REGISTER_FLAGS=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --out-dir) + OUT_DIR="${2:-}" + shift 2 + ;; + --release) + RELEASE="${2:-}" + shift 2 + ;; + --mirror) + MIRROR="${2:-}" + shift 2 + ;; + --arch) + ARCH="${2:-}" + shift 2 + ;; + --print-register-flags) + PRINT_REGISTER_FLAGS=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + log "unknown option: $1" + usage + exit 1 + ;; + esac +done + +if [[ "$PRINT_REGISTER_FLAGS" == "1" ]]; then + print_register_flags + exit 0 +fi + +if [[ "$ARCH" != "x86_64" ]]; then + log "unsupported arch: $ARCH" + log "this experimental builder currently supports only x86_64" + exit 1 +fi +if [[ -d "$OUT_DIR" ]]; then + log "output directory already exists: $OUT_DIR" + log "remove it first if you want to re-stage a different Alpine kernel" + exit 1 +fi + +require_command curl +require_command tar +require_command sha256sum +require_command install +require_command find +require_command cp +require_command readelf +require_command file +require_command tail +require_command grep +require_command cut +require_command gzip +require_command xz +require_command bzip2 + +if command -v unsquashfs >/dev/null 2>&1; then + USE_UNSQUASHFS=1 +else + USE_UNSQUASHFS=0 + require_command sudo + require_command mount + require_command umount +fi + +TMP_DIR="$(mktemp -d -t banger-alpine-kernel-XXXXXX)" +EXTRACT_DIR="$TMP_DIR/extract" +MODLOOP_DIR="$TMP_DIR/modloop" +MODLOOP_MOUNT="$TMP_DIR/modloop.mount" +ARCHIVE="$TMP_DIR/alpine-netboot.tar.gz" +MODLOOP_MOUNTED=0 +trap cleanup EXIT + +mkdir -p "$EXTRACT_DIR" "$MODLOOP_DIR" "$MODLOOP_MOUNT" + +BRANCH="$(resolve_release_branch "$RELEASE")" +RELEASE_DIR="$MIRROR/$BRANCH/releases/$ARCH" +ARCHIVE_URL="$RELEASE_DIR/alpine-netboot-$RELEASE-$ARCH.tar.gz" +SHA256_URL="$ARCHIVE_URL.sha256" + +log "downloading Alpine netboot bundle from $ARCHIVE_URL" +curl -fsSL "$ARCHIVE_URL" -o "$ARCHIVE" +expected_sha="$(curl -fsSL "$SHA256_URL" | awk '{print $1}')" +actual_sha="$(sha256sum "$ARCHIVE" | awk '{print $1}')" +if [[ -z "$expected_sha" ]]; then + log "failed to read SHA256 from $SHA256_URL" + exit 1 +fi +if [[ "$expected_sha" != "$actual_sha" ]]; then + log "sha256 mismatch for $ARCHIVE_URL" + log "expected: $expected_sha" + log "actual: $actual_sha" + exit 1 +fi + +VMLINUX_ENTRY="$(find_tar_entry "$ARCHIVE" 'vmlinuz-virt' || true)" +INITRD_ENTRY="$(find_tar_entry "$ARCHIVE" 'initramfs-virt' || true)" +MODLOOP_ENTRY="$(find_tar_entry "$ARCHIVE" 'modloop-virt' || true)" +CONFIG_ENTRY="$(find_tar_config_entry "$ARCHIVE" || true)" + +if [[ -z "$VMLINUX_ENTRY" || -z "$INITRD_ENTRY" || -z "$MODLOOP_ENTRY" ]]; then + log "Alpine netboot bundle is missing expected virt boot artifacts" + exit 1 +fi + +log "extracting Alpine virt boot artifacts" +tar_args=("$VMLINUX_ENTRY" "$INITRD_ENTRY" "$MODLOOP_ENTRY") +if [[ -n "$CONFIG_ENTRY" ]]; then + tar_args+=("$CONFIG_ENTRY") +fi +tar -xf "$ARCHIVE" -C "$EXTRACT_DIR" "${tar_args[@]}" + +VMLINUX_SRC="$EXTRACT_DIR/$VMLINUX_ENTRY" +INITRD_SRC="$EXTRACT_DIR/$INITRD_ENTRY" +MODLOOP_SRC="$EXTRACT_DIR/$MODLOOP_ENTRY" +CONFIG_SRC="" +if [[ -n "$CONFIG_ENTRY" ]]; then + CONFIG_SRC="$EXTRACT_DIR/$CONFIG_ENTRY" +fi + +if [[ "$USE_UNSQUASHFS" == "1" ]]; then + log "extracting kernel modules with unsquashfs" + unsquashfs -f -d "$MODLOOP_DIR" "$MODLOOP_SRC" >/dev/null +else + log "extracting kernel modules with a read-only loop mount" + sudo mount -o loop,ro "$MODLOOP_SRC" "$MODLOOP_MOUNT" + MODLOOP_MOUNTED=1 + cp -a "$MODLOOP_MOUNT/." "$MODLOOP_DIR/" + sudo umount "$MODLOOP_MOUNT" + MODLOOP_MOUNTED=0 +fi + +MODULES_ROOT="" +if [[ -d "$MODLOOP_DIR/modules" ]]; then + MODULES_ROOT="$MODLOOP_DIR/modules" +elif [[ -d "$MODLOOP_DIR/lib/modules" ]]; then + MODULES_ROOT="$MODLOOP_DIR/lib/modules" +fi +if [[ -z "$MODULES_ROOT" ]]; then + log "extracted modloop is missing a modules directory" + exit 1 +fi + +MODULES_SRC="$(find_latest_module_dir "$MODULES_ROOT" || true)" +if [[ -z "$MODULES_SRC" ]]; then + log "failed to locate a kernel modules tree inside modloop-virt" + exit 1 +fi + +KERNEL_VERSION="$(basename "$MODULES_SRC")" +mkdir -p "$OUT_DIR/boot" "$OUT_DIR/lib/modules" +install -m 0644 "$VMLINUX_SRC" "$OUT_DIR/boot/vmlinuz-$KERNEL_VERSION" +install -m 0644 "$INITRD_SRC" "$OUT_DIR/boot/initramfs-$KERNEL_VERSION.img" +if [[ -n "$CONFIG_SRC" && -f "$CONFIG_SRC" ]]; then + install -m 0644 "$CONFIG_SRC" "$OUT_DIR/boot/config-$KERNEL_VERSION" +fi +cp -a "$MODULES_SRC" "$OUT_DIR/lib/modules/" + +log "extracting Firecracker kernel from vmlinuz-$KERNEL_VERSION" +if ! extract_vmlinux "$VMLINUX_SRC" "$OUT_DIR/boot/vmlinux-$KERNEL_VERSION"; then + log "failed to extract an uncompressed vmlinux from $VMLINUX_SRC" + log "raw kernel image type: $(file -b "$VMLINUX_SRC")" + exit 1 +fi + +log "staged Alpine kernel artifacts in $OUT_DIR" +log "kernel version: $KERNEL_VERSION" diff --git a/scripts/make-rootfs-alpine.sh b/scripts/make-rootfs-alpine.sh new file mode 100755 index 0000000..48fa803 --- /dev/null +++ b/scripts/make-rootfs-alpine.sh @@ -0,0 +1,722 @@ +#!/usr/bin/env bash +set -euo pipefail + +log() { + printf '[make-rootfs-alpine] %s\n' "$*" +} + +usage() { + cat <<'EOF' +Usage: ./scripts/make-rootfs-alpine.sh [--out ] [--size ] [--release ] [--mirror ] [--arch ] + +Build an experimental Alpine Linux rootfs image plus a matching /root work-seed. + +Defaults: + --out ./build/manual/rootfs-alpine.ext4 + --size 2G + --release 3.23.3 + --mirror https://dl-cdn.alpinelinux.org/alpine + --arch x86_64 + +This path is experimental and local-only. If ./build/manual/alpine-kernel exists +it uses the staged Alpine kernel modules from that directory. It does not change +the default Debian image flow. +EOF +} + +parse_size() { + local raw="$1" + if [[ "$raw" =~ ^([0-9]+)([KMG])?$ ]]; then + local num="${BASH_REMATCH[1]}" + local unit="${BASH_REMATCH[2]}" + case "$unit" in + K) printf '%s\n' $((num * 1024)) ;; + M|"") printf '%s\n' $((num * 1024 * 1024)) ;; + G) printf '%s\n' $((num * 1024 * 1024 * 1024)) ;; + esac + return 0 + fi + return 1 +} + +require_command() { + local name="$1" + command -v "$name" >/dev/null 2>&1 || { + log "required command not found: $name" + exit 1 + } +} + +resolve_banger_bin() { + if [[ -n "${BANGER_BIN:-}" ]]; then + printf '%s\n' "$BANGER_BIN" + return + fi + if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then + printf '%s\n' "$REPO_ROOT/build/bin/banger" + return + fi + if [[ -x "$REPO_ROOT/banger" ]]; then + printf '%s\n' "$REPO_ROOT/banger" + return + fi + if command -v banger >/dev/null 2>&1; then + command -v banger + return + fi + log "banger binary not found; build it first with 'make build' or set BANGER_BIN" + exit 1 +} + +find_latest_module_dir() { + local root="$1" + if [[ ! -d "$root" ]]; then + return 1 + fi + find "$root" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1 +} + +resolve_release_branch() { + local release="$1" + printf 'v%s\n' "${release%.*}" +} + +load_package_preset() { + local preset="$1" + local -n out="$2" + mapfile -t out < <("$BANGER_BIN" internal packages "$preset") + (( ${#out[@]} > 0 )) +} + +write_rootfs_manifest_metadata() { + local rootfs_path="$1" + local manifest_hash="$2" + printf '%s\n' "$manifest_hash" > "${rootfs_path}.packages.sha256" +} + +install_root_authorized_key() { + local public_key + public_key="$(ssh-keygen -y -f "$SSH_KEY")" + sudo mkdir -p "$ROOT_MOUNT/root/.ssh" + printf '%s\n' "$public_key" | sudo tee "$ROOT_MOUNT/root/.ssh/authorized_keys" >/dev/null + sudo chmod 700 "$ROOT_MOUNT/root/.ssh" + sudo chmod 600 "$ROOT_MOUNT/root/.ssh/authorized_keys" +} + +ensure_sshd_include() { + local cfg="$ROOT_MOUNT/etc/ssh/sshd_config" + local tmp_cfg="$TMP_DIR/sshd_config" + local include_line="Include /etc/ssh/sshd_config.d/*.conf" + + sudo mkdir -p "$ROOT_MOUNT/etc/ssh/sshd_config.d" + if sudo test -f "$cfg"; then + sudo cat "$cfg" > "$tmp_cfg" + else + : > "$tmp_cfg" + fi + + if ! grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "$tmp_cfg"; then + { + printf '%s\n' "$include_line" + cat "$tmp_cfg" + } > "${tmp_cfg}.new" + mv "${tmp_cfg}.new" "$tmp_cfg" + sudo install -m 0644 "$tmp_cfg" "$cfg" + fi +} + +normalize_root_shell() { + local passwd="$ROOT_MOUNT/etc/passwd" + local shells="$ROOT_MOUNT/etc/shells" + local wanted_shell="/bin/bash" + local tmp_passwd="$TMP_DIR/passwd" + local root_shell="" + + if [[ ! -x "$ROOT_MOUNT$wanted_shell" ]]; then + log "required root shell is missing from the Alpine image: $wanted_shell" + exit 1 + fi + if [[ ! -f "$shells" ]]; then + log "Alpine image is missing /etc/shells" + exit 1 + fi + if ! sudo grep -Fxq "$wanted_shell" "$shells"; then + log "Alpine image does not allow $wanted_shell in /etc/shells" + exit 1 + fi + + sudo cat "$passwd" > "$tmp_passwd" + awk -F: -v OFS=: -v shell="$wanted_shell" ' + $1 == "root" { + $7 = shell + found = 1 + } + { print } + END { + if (!found) { + exit 1 + } + } + ' "$tmp_passwd" > "${tmp_passwd}.new" || { + log "failed to rewrite root shell in /etc/passwd" + exit 1 + } + mv "${tmp_passwd}.new" "$tmp_passwd" + sudo install -m 0644 "$tmp_passwd" "$passwd" + + root_shell="$(sudo awk -F: '$1 == "root" { print $7 }' "$passwd")" + if [[ "$root_shell" != "$wanted_shell" ]]; then + log "root shell normalization failed: expected $wanted_shell, got ${root_shell:-}" + exit 1 + fi +} + +configure_root_bash_prompt() { + local bashrc="$ROOT_MOUNT/root/.bashrc" + local bash_profile="$ROOT_MOUNT/root/.bash_profile" + local profile_prompt="$ROOT_MOUNT/etc/profile.d/banger-bash-prompt.sh" + + sudo mkdir -p "$ROOT_MOUNT/root" "$ROOT_MOUNT/etc/profile.d" + cat <<'EOF' | sudo tee "$bashrc" >/dev/null +# banger: default interactive prompt for experimental Alpine guests +case "$-" in + *i*) ;; + *) return ;; +esac + +if [ -z "${BANGER_MISE_ACTIVATED:-}" ] && [ -x '/usr/local/bin/mise' ]; then + export BANGER_MISE_ACTIVATED=1 + eval "$(/usr/local/bin/mise activate bash)" +fi + +PS1='\u@\h:\w\$ ' +EOF + cat <<'EOF' | sudo tee "$bash_profile" >/dev/null +if [ -f ~/.bashrc ]; then + . ~/.bashrc +fi +EOF + cat <<'EOF' | sudo tee "$profile_prompt" >/dev/null +case "$-" in + *i*) ;; + *) return 0 2>/dev/null || exit 0 ;; +esac + +if [ -n "${BASH_VERSION:-}" ]; then + PS1='\u@\h:\w\$ ' +fi +EOF + sudo chmod 0644 "$bashrc" "$bash_profile" "$profile_prompt" +} + +install_guest_network_bootstrap() { + sudo mkdir -p "$ROOT_MOUNT/usr/local/libexec" + sudo install -m 0755 "$GUESTNET_BOOTSTRAP_SCRIPT" "$ROOT_MOUNT/usr/local/libexec/banger-network-bootstrap" +} + +install_openrc_services() { + local initd_dir="$ROOT_MOUNT/etc/init.d" + + sudo mkdir -p "$initd_dir" + + cat <<'EOF' | sudo tee "$initd_dir/banger-network" >/dev/null +#!/sbin/openrc-run +description="Banger guest network bootstrap" + +depend() { + need localmount + before sshd docker banger-vsock-agent banger-opencode + provide net +} + +start() { + ebegin "Configuring guest network" + /usr/local/libexec/banger-network-bootstrap + eend $? +} +EOF + + cat <<'EOF' | sudo tee "$initd_dir/banger-docker-preflight" >/dev/null +#!/sbin/openrc-run +description="Banger Docker kernel preflight" + +depend() { + after modules + before docker +} + +start() { + ebegin "Preparing Docker kernel state" + for module in nf_tables nft_chain_nat veth br_netfilter overlay; do + modprobe "$module" 2>/dev/null || true + done + if command -v sysctl >/dev/null 2>&1; then + sysctl -p /etc/sysctl.d/99-docker.conf >/dev/null 2>&1 || true + fi + eend 0 +} +EOF + + cat <<'EOF' | sudo tee "$initd_dir/banger-vsock-agent" >/dev/null +#!/sbin/openrc-run +description="Banger vsock agent" +pidfile="/run/${RC_SVCNAME}.pid" +command="/usr/local/bin/banger-vsock-agent" + +depend() { + need localmount + after banger-network +} + +start_pre() { + modprobe vsock 2>/dev/null || true + modprobe vmw_vsock_virtio_transport 2>/dev/null || true +} + +start() { + ebegin "Starting ${RC_SVCNAME}" + start-stop-daemon --start --exec "$command" --background --make-pidfile --pidfile "$pidfile" + eend $? +} + +stop() { + ebegin "Stopping ${RC_SVCNAME}" + start-stop-daemon --stop --exec "$command" --pidfile "$pidfile" + eend $? +} +EOF + + cat <<'EOF' | sudo tee "$initd_dir/banger-opencode" >/dev/null +#!/sbin/openrc-run +description="Banger opencode server" +pidfile="/run/${RC_SVCNAME}.pid" +command="/usr/local/bin/opencode" +command_args="serve --hostname 0.0.0.0 --port 4096" + +depend() { + need localmount + after banger-network +} + +start() { + ebegin "Starting ${RC_SVCNAME}" + HOME=/root start-stop-daemon --start --exec "$command" --background --make-pidfile --pidfile "$pidfile" --chdir /root -- $command_args + eend $? +} + +stop() { + ebegin "Stopping ${RC_SVCNAME}" + start-stop-daemon --stop --exec "$command" --pidfile "$pidfile" + eend $? +} +EOF + + sudo chmod 0755 \ + "$initd_dir/banger-network" \ + "$initd_dir/banger-docker-preflight" \ + "$initd_dir/banger-vsock-agent" \ + "$initd_dir/banger-opencode" +} + +configure_docker_bootstrap() { + local modules_conf="$ROOT_MOUNT/etc/modules-load.d/docker-netfilter.conf" + local sysctl_conf="$ROOT_MOUNT/etc/sysctl.d/99-docker.conf" + + sudo mkdir -p "$ROOT_MOUNT/etc/modules-load.d" "$ROOT_MOUNT/etc/sysctl.d" + cat <<'EOF' | sudo tee "$modules_conf" >/dev/null +nf_tables +nft_chain_nat +veth +br_netfilter +overlay +EOF + cat <<'EOF' | sudo tee "$sysctl_conf" >/dev/null +net.bridge.bridge-nf-call-iptables = 1 +net.bridge.bridge-nf-call-ip6tables = 1 +net.ipv4.ip_forward = 1 +EOF + sudo chmod 0644 "$modules_conf" "$sysctl_conf" +} + +configure_vsock_modules() { + local modules_conf="$ROOT_MOUNT/etc/modules-load.d/banger-vsock.conf" + + sudo mkdir -p "$ROOT_MOUNT/etc/modules-load.d" + cat <<'EOF' | sudo tee "$modules_conf" >/dev/null +vsock +vmw_vsock_virtio_transport +EOF + sudo chmod 0644 "$modules_conf" +} + +configure_apk_repositories() { + local repositories="$ROOT_MOUNT/etc/apk/repositories" + + sudo mkdir -p "$ROOT_MOUNT/etc/apk" + cat </dev/null +$APK_RELEASE_URL/main +$APK_RELEASE_URL/community +EOF + sudo chmod 0644 "$repositories" + if [[ -r /etc/resolv.conf ]]; then + sudo install -m 0644 /etc/resolv.conf "$ROOT_MOUNT/etc/resolv.conf" + fi +} + +build_alpine_initramfs() { + local kernel_version="$1" + local guest_output="/boot/initramfs-${kernel_version}.img" + local stage_output="$MANUAL_DIR/alpine-kernel/boot/initramfs-${kernel_version}.img" + local mkinitfs_dir="$ROOT_MOUNT/etc/mkinitfs" + local mkinitfs_conf="$mkinitfs_dir/mkinitfs.conf" + + sudo mkdir -p "$mkinitfs_dir" "$ROOT_MOUNT/boot" "$MANUAL_DIR/alpine-kernel/boot" + cat <<'EOF' | sudo tee "$mkinitfs_conf" >/dev/null +features="ata base ide scsi usb virtio ext4 nvme" +EOF + sudo chmod 0644 "$mkinitfs_conf" + + log "building Alpine initramfs for kernel $kernel_version" + sudo env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + chroot "$ROOT_MOUNT" /bin/sh -se <&2 + exit 1 +fi +ln -snf /root/.local/share/mise/shims/opencode /usr/local/bin/opencode +EOF + + cat <<'EOF' | sudo tee "$profile_mise" >/dev/null +if [ -n "${BASH_VERSION:-}" ] && [ -z "${BANGER_MISE_ACTIVATED:-}" ] && [ -x '/usr/local/bin/mise' ]; then + export BANGER_MISE_ACTIVATED=1 + eval "$(/usr/local/bin/mise activate bash)" +fi +EOF + sudo chmod 0644 "$profile_mise" +} + +enable_openrc_services() { + sudo env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin chroot "$ROOT_MOUNT" /bin/sh -se <<'EOF' +set -eu + +add_service() { + local service="$1" + local runlevel="$2" + if [ ! -x "/etc/init.d/$service" ]; then + echo "missing OpenRC service: $service" >&2 + exit 1 + fi + rc-update add "$service" "$runlevel" >/dev/null +} + +for service in devfs dmesg mdev; do + add_service "$service" sysinit +done +for service in hwdrivers modules sysctl hostname bootmisc cgroups; do + add_service "$service" boot +done +for service in banger-network sshd banger-docker-preflight docker banger-vsock-agent banger-opencode; do + add_service "$service" default +done +for service in mount-ro killprocs; do + add_service "$service" shutdown +done +EOF +} + +cleanup() { + if [[ "${SYS_MOUNTED:-0}" == "1" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT/sys"; then + sudo umount "$ROOT_MOUNT/sys" || true + fi + if [[ "${PROC_MOUNTED:-0}" == "1" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT/proc"; then + sudo umount "$ROOT_MOUNT/proc" || true + fi + if [[ "${DEVPTS_MOUNTED:-0}" == "1" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT/dev/pts"; then + sudo umount "$ROOT_MOUNT/dev/pts" || true + fi + if [[ "${DEV_MOUNTED:-0}" == "1" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT/dev"; then + sudo umount "$ROOT_MOUNT/dev" || true + fi + if [[ -n "${ROOT_MOUNT:-}" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT"; then + sudo umount "$ROOT_MOUNT" || true + fi + if [[ "${BUILD_DONE:-0}" != "1" ]]; then + rm -f "${OUT_ROOTFS:-}" "${WORK_SEED:-}" "${OUT_ROOTFS:-}.packages.sha256" + fi + if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then + rm -rf "$TMP_DIR" + fi +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +MANUAL_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}" +BANGER_BIN="$(resolve_banger_bin)" +SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)" +OUT_ROOTFS="$MANUAL_DIR/rootfs-alpine.ext4" +SIZE_SPEC="2G" +RELEASE="${ALPINE_RELEASE:-3.23.3}" +MIRROR="https://dl-cdn.alpinelinux.org/alpine" +ARCH="x86_64" +MISE_VERSION="v2025.12.0" +MISE_INSTALL_PATH="/usr/local/bin/mise" +OPENCODE_TOOL="github:anomalyco/opencode" +GUESTNET_BOOTSTRAP_SCRIPT="$REPO_ROOT/internal/guestnet/assets/bootstrap.sh" +MODULES_DIR="" +ALPINE_KERNEL_MODULES_DIR="$(find_latest_module_dir "$MANUAL_DIR/alpine-kernel/lib/modules" || true)" +VSOCK_AGENT="$("$BANGER_BIN" internal vsock-agent-path)" +if [[ -n "$ALPINE_KERNEL_MODULES_DIR" ]]; then + MODULES_DIR="$ALPINE_KERNEL_MODULES_DIR" +fi + +while [[ $# -gt 0 ]]; do + case "$1" in + --out) + OUT_ROOTFS="${2:-}" + shift 2 + ;; + --size) + SIZE_SPEC="${2:-}" + shift 2 + ;; + --release) + RELEASE="${2:-}" + shift 2 + ;; + --mirror) + MIRROR="${2:-}" + shift 2 + ;; + --arch) + ARCH="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + log "unknown option: $1" + usage + exit 1 + ;; + esac +done + +if [[ "$ARCH" != "x86_64" ]]; then + log "unsupported arch: $ARCH" + log "this experimental builder currently supports only x86_64" + exit 1 +fi + +if [[ -z "$MODULES_DIR" || ! -d "$MODULES_DIR" ]]; then + log "modules dir not found; run 'make alpine-kernel' first" + exit 1 +fi +if [[ ! -x "$VSOCK_AGENT" ]]; then + log "vsock agent not found or not executable: $VSOCK_AGENT" + log "run 'make build'" + exit 1 +fi +if [[ ! -f "$GUESTNET_BOOTSTRAP_SCRIPT" ]]; then + log "guest network bootstrap script not found: $GUESTNET_BOOTSTRAP_SCRIPT" + exit 1 +fi +if [[ -e "$OUT_ROOTFS" ]]; then + log "output rootfs already exists: $OUT_ROOTFS" + exit 1 +fi + +require_command curl +require_command tar +require_command sudo +require_command mkfs.ext4 +require_command ssh-keygen +require_command mount +require_command umount +require_command install +require_command find +require_command awk +require_command sed +require_command sha256sum +require_command truncate +require_command mountpoint +require_command chroot +require_command cp + +ALPINE_PACKAGES=() +if ! load_package_preset alpine ALPINE_PACKAGES; then + log "alpine package preset is empty" + exit 1 +fi +if ! PACKAGES_HASH="$(printf '%s\n' "${ALPINE_PACKAGES[@]}" | sha256sum | awk '{print $1}')"; then + log "failed to hash package preset" + exit 1 +fi +if ! SIZE_BYTES="$(parse_size "$SIZE_SPEC")"; then + log "invalid size: $SIZE_SPEC" + exit 1 +fi + +if [[ "$OUT_ROOTFS" == *.ext4 ]]; then + WORK_SEED="${OUT_ROOTFS%.ext4}.work-seed.ext4" +else + WORK_SEED="${OUT_ROOTFS}.work-seed" +fi + +BRANCH="$(resolve_release_branch "$RELEASE")" +RELEASE_DIR="$MIRROR/$BRANCH/releases/$ARCH" +MINIROOTFS_URL="$RELEASE_DIR/alpine-minirootfs-$RELEASE-$ARCH.tar.gz" +MINIROOTFS_SHA256_URL="$MINIROOTFS_URL.sha256" +APK_RELEASE_URL="$MIRROR/$BRANCH" + +TMP_DIR="$(mktemp -d -t banger-alpine-rootfs-XXXXXX)" +MINIROOTFS_ARCHIVE="$TMP_DIR/alpine-minirootfs.tar.gz" +ROOT_MOUNT="$TMP_DIR/rootfs" +BUILD_DONE=0 +DEV_MOUNTED=0 +DEVPTS_MOUNTED=0 +PROC_MOUNTED=0 +SYS_MOUNTED=0 +trap cleanup EXIT + +mkdir -p "$ROOT_MOUNT" + +log "downloading Alpine minirootfs from $MINIROOTFS_URL" +curl -fsSL "$MINIROOTFS_URL" -o "$MINIROOTFS_ARCHIVE" +expected_sha="$(curl -fsSL "$MINIROOTFS_SHA256_URL" | awk '{print $1}')" +actual_sha="$(sha256sum "$MINIROOTFS_ARCHIVE" | awk '{print $1}')" +if [[ -z "$expected_sha" ]]; then + log "failed to read SHA256 from $MINIROOTFS_SHA256_URL" + exit 1 +fi +if [[ "$expected_sha" != "$actual_sha" ]]; then + log "sha256 mismatch for $MINIROOTFS_URL" + log "expected: $expected_sha" + log "actual: $actual_sha" + exit 1 +fi + +log "creating $OUT_ROOTFS ($SIZE_SPEC)" +truncate -s "$SIZE_BYTES" "$OUT_ROOTFS" +mkfs.ext4 -F -m 0 -L banger-alpine-root "$OUT_ROOTFS" >/dev/null +sudo mount -o loop "$OUT_ROOTFS" "$ROOT_MOUNT" + +log "unpacking Alpine minirootfs" +sudo tar -xzf "$MINIROOTFS_ARCHIVE" -C "$ROOT_MOUNT" +configure_apk_repositories +mount_chroot_support + +log "installing Alpine packages into the rootfs" +sudo env HOME=/root PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin \ + chroot "$ROOT_MOUNT" /bin/sh -se <&2 +} + +find_latest_matching() { + local dir="$1" + local pattern="$2" + if [[ ! -d "$dir" ]]; then + return 1 + fi + find "$dir" -maxdepth 1 -type f -name "$pattern" | sort | tail -n 1 +} + +find_latest_module_dir() { + local root="$1" + if [[ ! -d "$root" ]]; then + return 1 + fi + find "$root" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1 +} + +resolve_banger_bin() { + if [[ -n "${BANGER_BIN:-}" ]]; then + printf '%s\n' "$BANGER_BIN" + return + fi + if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then + printf '%s\n' "$REPO_ROOT/build/bin/banger" + return + fi + if [[ -x "$REPO_ROOT/banger" ]]; then + printf '%s\n' "$REPO_ROOT/banger" + return + fi + if command -v banger >/dev/null 2>&1; then + command -v banger + return + fi + log "banger binary not found; build it first with 'make build' or set BANGER_BIN" + exit 1 +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +RUNTIME_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}" +IMAGE_NAME="${ALPINE_IMAGE_NAME:-alpine}" +BANGER_BIN="$(resolve_banger_bin)" +ROOTFS="$RUNTIME_DIR/rootfs-alpine.ext4" +WORK_SEED="$RUNTIME_DIR/rootfs-alpine.work-seed.ext4" + +if [[ ! -f "$ROOTFS" ]]; then + log "missing Alpine rootfs: $ROOTFS" + exit 1 +fi +if [[ ! -f "$WORK_SEED" ]]; then + log "missing Alpine work-seed: $WORK_SEED" + exit 1 +fi + +args=( + image register + --name "$IMAGE_NAME" + --rootfs "$ROOTFS" + --work-seed "$WORK_SEED" + --docker +) + +if [[ ! -d "$RUNTIME_DIR/alpine-kernel" ]]; then + log "missing staged Alpine kernel artifacts: $RUNTIME_DIR/alpine-kernel" + log "run 'make alpine-kernel' before registering $IMAGE_NAME" + exit 1 +fi + +kernel="$(find_latest_matching "$RUNTIME_DIR/alpine-kernel/boot" 'vmlinux-*' || true)" +if [[ -z "$kernel" ]]; then + kernel="$(find_latest_matching "$RUNTIME_DIR/alpine-kernel/boot" 'vmlinuz-*' || true)" +fi +initrd="$(find_latest_matching "$RUNTIME_DIR/alpine-kernel/boot" 'initramfs-*' || true)" +modules="$(find_latest_module_dir "$RUNTIME_DIR/alpine-kernel/lib/modules" || true)" + +if [[ -z "$kernel" || -z "$initrd" || -z "$modules" ]]; then + log "staged Alpine kernel is incomplete; expected kernel, initramfs, and modules under $RUNTIME_DIR/alpine-kernel" + exit 1 +fi + +log "using staged Alpine kernel artifacts from $RUNTIME_DIR/alpine-kernel" +args+=(--kernel "$kernel" --initrd "$initrd" --modules "$modules") + +"$BANGER_BIN" "${args[@]}"