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.
This commit is contained in:
Thales Maciel 2026-03-21 20:25:55 -03:00
parent 572bf32424
commit a166068fab
No known key found for this signature in database
GPG key ID: 33112E6833C34679
14 changed files with 1307 additions and 9 deletions

View file

@ -17,10 +17,13 @@ BINARIES := $(BANGER_BIN) $(BANGERD_BIN) $(VSOCK_AGENT_BIN)
GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort) GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
VOID_IMAGE_NAME ?= void-exp VOID_IMAGE_NAME ?= void-exp
VOID_VM_NAME ?= void-dev VOID_VM_NAME ?= void-dev
ALPINE_RELEASE ?= 3.23.3
ALPINE_IMAGE_NAME ?= alpine
ALPINE_VM_NAME ?= alpine-dev
.DEFAULT_GOAL := help .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: help:
@printf '%s\n' \ @printf '%s\n' \
@ -37,7 +40,12 @@ help:
' make rootfs-void Build an experimental Void Linux rootfs and work-seed in ./build/manual' \ ' 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-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 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) build: $(BINARIES)
@ -92,3 +100,18 @@ void-vm: void-register
verify-void: void-register verify-void: void-register
BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/verify.sh --image "$(VOID_IMAGE_NAME)" 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)"

View file

@ -189,6 +189,46 @@ That flow uses:
- `./build/manual/rootfs-void.ext4` - `./build/manual/rootfs-void.ext4`
- `./build/manual/rootfs-void.work-seed.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 ## Notes
- Firecracker is resolved from `PATH` by default. - Firecracker is resolved from `PATH` by default.

View file

@ -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"

View file

@ -175,9 +175,9 @@ func newInternalVSockAgentPathCommand() *cobra.Command {
func newInternalPackagesCommand() *cobra.Command { func newInternalPackagesCommand() *cobra.Command {
var docker bool var docker bool
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "packages <debian|void>", Use: "packages <debian|void|alpine>",
Hidden: true, Hidden: true,
Args: exactArgsUsage(1, "usage: banger internal packages <debian|void> [--docker]"), Args: exactArgsUsage(1, "usage: banger internal packages <debian|void|alpine> [--docker]"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
var packages []string var packages []string
switch strings.TrimSpace(args[0]) { switch strings.TrimSpace(args[0]) {
@ -188,6 +188,8 @@ func newInternalPackagesCommand() *cobra.Command {
} }
case "void": case "void":
packages = imagepreset.VoidBasePackages() packages = imagepreset.VoidBasePackages()
case "alpine":
packages = imagepreset.AlpineBasePackages()
default: default:
return fmt.Errorf("unknown package preset %q", args[0]) return fmt.Errorf("unknown package preset %q", args[0])
} }

View file

@ -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) { func TestVMCreateFlagsExist(t *testing.T) {
root := NewBangerCommand() root := NewBangerCommand()
vm, _, err := root.Find([]string{"vm"}) vm, _, err := root.Find([]string{"vm"})

View file

@ -43,6 +43,31 @@ var voidBase = []string{
"wget", "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 { func DebianBasePackages() []string {
return append([]string(nil), debianBase...) return append([]string(nil), debianBase...)
} }
@ -51,6 +76,10 @@ func VoidBasePackages() []string {
return append([]string(nil), voidBase...) return append([]string(nil), voidBase...)
} }
func AlpineBasePackages() []string {
return append([]string(nil), alpineBase...)
}
func Hash(lines []string) string { func Hash(lines []string) string {
sum := sha256.Sum256([]byte(strings.Join(lines, "\n") + "\n")) sum := sha256.Sum256([]byte(strings.Join(lines, "\n") + "\n"))
return fmt.Sprintf("%x", sum) return fmt.Sprintf("%x", sum)

View file

@ -17,7 +17,7 @@ const (
ShimPath = "/root/.local/share/mise/shims/opencode" ShimPath = "/root/.local/share/mise/shims/opencode"
ServiceName = "banger-opencode.service" ServiceName = "banger-opencode.service"
RunitServiceName = "banger-opencode" RunitServiceName = "banger-opencode"
ReadyTimeout = 15 * time.Second ReadyTimeout = 45 * time.Second
pollInterval = 200 * time.Millisecond pollInterval = 200 * time.Millisecond
) )

View file

@ -417,7 +417,7 @@ func UpdateFSTab(existing string) string {
func BuildBootArgs(vmName, guestIP, bridgeIP, dns string) string { func BuildBootArgs(vmName, guestIP, bridgeIP, dns string) string {
return fmt.Sprintf( 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, guestIP,
bridgeIP, bridgeIP,
vmName, vmName,

View file

@ -171,7 +171,7 @@ func TestBuildBootArgsIncludesHostnameInIPField(t *testing.T) {
t.Parallel() t.Parallel()
got := BuildBootArgs("devbox", "172.16.0.2", "172.16.0.1", "1.1.1.1") 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 { if got != want {
t.Fatalf("BuildBootArgs() = %q, want %q", got, want) t.Fatalf("BuildBootArgs() = %q, want %q", got, want)
} }

View file

@ -303,7 +303,7 @@ sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \
"smt": false "smt": false
}' >/dev/null }' >/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="" INITRD_JSON=""
if [[ -n "$INITRD" ]]; then if [[ -n "$INITRD" ]]; then

View file

@ -230,7 +230,7 @@ sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \
"smt": false "smt": false
}' >/dev/null }' >/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 \ sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \

363
scripts/make-alpine-kernel.sh Executable file
View file

@ -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 <path>] [--release <x.y.z>] [--mirror <url>] [--arch <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-<version> Alpine virt kernel image
boot/initramfs-<version>.img Matching Alpine initramfs
boot/config-<version> Alpine kernel config when present
lib/modules/<version>/ 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"

722
scripts/make-rootfs-alpine.sh Executable file
View file

@ -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 <path>] [--size <size>] [--release <x.y.z>] [--mirror <url>] [--arch <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:-<empty>}"
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 <<EOF | sudo tee "$repositories" >/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 <<EOF
set -eu
mkinitfs -c /etc/mkinitfs/mkinitfs.conf -b / -o "$guest_output" "$kernel_version"
EOF
if [[ ! -f "$ROOT_MOUNT$guest_output" ]]; then
log "mkinitfs did not produce $guest_output inside the guest rootfs"
exit 1
fi
sudo install -m 0644 "$ROOT_MOUNT$guest_output" "$stage_output"
}
mount_chroot_support() {
sudo mkdir -p "$ROOT_MOUNT/dev" "$ROOT_MOUNT/dev/pts" "$ROOT_MOUNT/proc" "$ROOT_MOUNT/sys"
sudo mount --bind /dev "$ROOT_MOUNT/dev"
DEV_MOUNTED=1
sudo mount --bind /dev/pts "$ROOT_MOUNT/dev/pts"
DEVPTS_MOUNTED=1
sudo mount -t proc proc "$ROOT_MOUNT/proc"
PROC_MOUNTED=1
sudo mount -t sysfs sys "$ROOT_MOUNT/sys"
SYS_MOUNTED=1
}
install_mise_and_opencode() {
local profile_mise="$ROOT_MOUNT/etc/profile.d/mise.sh"
sudo mkdir -p "$ROOT_MOUNT/etc/profile.d"
if [[ -r /etc/resolv.conf ]]; then
sudo install -m 0644 /etc/resolv.conf "$ROOT_MOUNT/etc/resolv.conf"
fi
sudo env \
HOME=/root \
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin \
chroot "$ROOT_MOUNT" /bin/sh -se <<EOF
set -eu
curl -fsSL https://mise.run | MISE_INSTALL_PATH="$MISE_INSTALL_PATH" MISE_VERSION="$MISE_VERSION" sh
"$MISE_INSTALL_PATH" use -g "$OPENCODE_TOOL"
"$MISE_INSTALL_PATH" reshim
if [ ! -e /root/.local/share/mise/shims/opencode ]; then
echo "opencode shim not found after mise install" >&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 <<EOF
set -eu
apk update
apk add --no-cache ${ALPINE_PACKAGES[*]}
EOF
log "copying staged Alpine kernel modules into the guest"
sudo mkdir -p "$ROOT_MOUNT/lib/modules"
sudo cp -a "$MODULES_DIR" "$ROOT_MOUNT/lib/modules/"
KERNEL_VERSION="$(basename "$MODULES_DIR")"
log "installing the guest-side vsock agent"
sudo mkdir -p "$ROOT_MOUNT/usr/local/bin"
sudo install -m 0755 "$VSOCK_AGENT" "$ROOT_MOUNT/usr/local/bin/banger-vsock-agent"
log "preparing SSH, OpenRC, and Docker bootstrap"
install_guest_network_bootstrap
install_openrc_services
ensure_sshd_include
configure_docker_bootstrap
configure_vsock_modules
normalize_root_shell
configure_root_bash_prompt
log "installing mise and opencode"
install_mise_and_opencode
install_root_authorized_key
build_alpine_initramfs "$KERNEL_VERSION"
sudo touch "$ROOT_MOUNT/etc/fstab" "$ROOT_MOUNT/etc/hostname"
sudo chroot "$ROOT_MOUNT" /usr/bin/ssh-keygen -A
enable_openrc_services
sudo chroot "$ROOT_MOUNT" /bin/sh -se <<'EOF'
set -eu
git config --system init.defaultBranch main
EOF
log "removing bulky caches, docs, and stale installer artifacts from the experimental image"
sudo rm -rf \
"$ROOT_MOUNT/var/cache/apk" \
"$ROOT_MOUNT/usr/share/doc" \
"$ROOT_MOUNT/usr/share/info" \
"$ROOT_MOUNT/usr/share/man"
sudo rm -f \
"$ROOT_MOUNT/root/get-docker" \
"$ROOT_MOUNT/root/get-docker.sh" \
"$ROOT_MOUNT/tmp/get-docker" \
"$ROOT_MOUNT/tmp/get-docker.sh"
sudo rm -rf \
"$ROOT_MOUNT/root/.cache/mise" \
"$ROOT_MOUNT/root/.cache/opencode" \
"$ROOT_MOUNT/root/.local/share/mise/downloads" \
"$ROOT_MOUNT/root/.local/share/mise/tmp"
sudo umount "$ROOT_MOUNT/sys"
SYS_MOUNTED=0
sudo umount "$ROOT_MOUNT/proc"
PROC_MOUNTED=0
sudo umount "$ROOT_MOUNT/dev/pts"
DEVPTS_MOUNTED=0
sudo umount "$ROOT_MOUNT/dev"
DEV_MOUNTED=0
sudo umount "$ROOT_MOUNT"
write_rootfs_manifest_metadata "$OUT_ROOTFS" "$PACKAGES_HASH"
log "building work-seed $WORK_SEED"
"$BANGER_BIN" internal work-seed --rootfs "$OUT_ROOTFS" --out "$WORK_SEED"
BUILD_DONE=1
log "built experimental Alpine rootfs: $OUT_ROOTFS"
log "built experimental Alpine work-seed: $WORK_SEED"

View file

@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[register-alpine-image] %s\n' "$*" >&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[@]}"