banger/scripts/make-rootfs-alpine.sh
Thales Maciel 092d848620
Wait for real guest vsock health before opencode
Make vm create wait for the guest-side vsock /healthz endpoint instead of only waiting for the host socket path, so the wait_vsock_agent stage reflects actual guest readiness.

Start banger-vsock-agent earlier in the Alpine OpenRC graph and report later /ports failures as guest-service waits rather than vsock-agent waits, which makes the progress output match what the guest is really doing.

Validate with go test ./..., a rebuilt managed alpine image, and a fresh vm create --image alpine --name alp --nat that now progresses through wait_vsock_agent -> wait_guest_ready -> wait_opencode -> ready.
2026-03-21 21:14:22 -03:00

722 lines
19 KiB
Bash
Executable file

#!/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-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
before banger-network sshd docker banger-opencode
}
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"