Prune legacy void/alpine + customize.sh flows

The golden-image Dockerfile + catalog pipeline replaces the entire
manual rootfs-build stack. With that shipped, the per-distro shell
flows are dead code.

Removed:
- scripts/customize.sh, scripts/interactive.sh, scripts/verify.sh
- scripts/make-rootfs{,-void,-alpine}.sh
- scripts/register-{void,alpine}-image.sh
- scripts/make-{void,alpine}-kernel.sh
- internal/imagepreset/ (only consumer was `banger internal packages`,
  which fed customize.sh)
- examples/{void,alpine}.config.toml
- Makefile targets: rootfs, rootfs-void, rootfs-alpine, void-kernel,
  alpine-kernel, void-register, alpine-register, void-vm, alpine-vm,
  verify-void, verify-alpine, plus the ALPINE_RELEASE / *_IMAGE_NAME
  / *_VM_NAME variables

The void-6.12 kernel catalog entry is also gone — golden image pairs
with generic-6.12 and nothing else in the catalog depended on it.

Consolidated: imagemgr now holds the small DebianBasePackages list +
package-hash helper inline, so the `image build --from-image` flow
(still supported) no longer pulls from a separate imagepreset package.

Net: 3,815 lines deleted, 59 added. No runtime functionality removed
beyond the `banger internal packages` subcommand (hidden, used only
by the deleted customize.sh).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-18 15:39:53 -03:00
parent 8029b2e1bc
commit 6083e2dde5
No known key found for this signature in database
GPG key ID: 33112E6833C34679
23 changed files with 73 additions and 3814 deletions

View file

@ -21,11 +21,6 @@ GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
# any redundant invocations.
BUILD_INPUTS := $(shell find cmd internal -type f | sort)
SHELL_SOURCES := $(shell find scripts -type f -name '*.sh' | sort)
VOID_IMAGE_NAME ?= void
VOID_VM_NAME ?= void-dev
ALPINE_RELEASE ?= 3.23.3
ALPINE_IMAGE_NAME ?= alpine
ALPINE_VM_NAME ?= alpine-dev
VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
COMMIT ?= $(shell git rev-parse --verify HEAD 2>/dev/null || echo unknown)
BUILT_AT ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
@ -33,30 +28,19 @@ GO_LDFLAGS := -X banger/internal/buildinfo.Version=$(VERSION) -X banger/internal
.DEFAULT_GOAL := help
.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 lint lint-go lint-shell
.PHONY: help build banger bangerd test fmt tidy clean install bench-create lint lint-go lint-shell
help:
@printf '%s\n' \
'Targets:' \
' make build Build ./build/bin/banger, ./build/bin/bangerd, and ./build/bin/banger-vsock-agent' \
' make bench-create Benchmark vm create and SSH readiness with scripts/bench-create.sh' \
' make install Build and install banger, bangerd, and the companion vsock helper' \
' make test Run go test ./...' \
' make lint Run gofmt + go vet + shellcheck (errors)' \
' make fmt Format Go sources under cmd/ and internal/' \
' make tidy Run go mod tidy' \
' make clean Remove built Go binaries' \
' make rootfs Rebuild the manual Debian rootfs image in ./build/manual' \
' make void-kernel Download and stage a Void kernel, initramfs, and modules under ./build/manual/void-kernel' \
' 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 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'
' make bench-create Benchmark vm create and SSH readiness with scripts/bench-create.sh'
build: $(BINARIES)
@ -107,36 +91,3 @@ install: build
$(INSTALL) -m 0755 "$(BANGER_BIN)" "$(DESTDIR)$(BINDIR)/banger"
$(INSTALL) -m 0755 "$(BANGERD_BIN)" "$(DESTDIR)$(BINDIR)/bangerd"
$(INSTALL) -m 0755 "$(VSOCK_AGENT_BIN)" "$(DESTDIR)$(LIBDIR)/banger/banger-vsock-agent"
rootfs:
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/make-rootfs.sh $(ARGS)
void-kernel:
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" ./scripts/make-void-kernel.sh $(ARGS)
rootfs-void:
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/make-rootfs-void.sh $(ARGS)
void-register: build
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" VOID_IMAGE_NAME="$(VOID_IMAGE_NAME)" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/register-void-image.sh
void-vm: void-register
"$(abspath $(BANGER_BIN))" vm create --image "$(VOID_IMAGE_NAME)" --name "$(VOID_VM_NAME)"
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)"

View file

@ -36,13 +36,8 @@ traversal entries and unsafe symlinks are rejected.
**`generic-<version>`** — built from upstream kernel.org sources with
Firecracker's official config. All essential drivers (virtio_blk,
virtio_net, ext4, vsock) compiled in — no modules, no initramfs. This
is the recommended kernel for OCI-pulled images (Debian, Ubuntu,
Fedora, etc.). Build with `scripts/make-generic-kernel.sh`.
**`void-<version>` / `alpine-<version>`** — distro-specific kernels
built from Void/Alpine package repos. Include initramfs + modules.
These are for the `make rootfs-void` / `make rootfs-alpine` manual
flows where the initramfs is paired with its matching rootfs.
is the kernel the golden image pairs with and the recommended kernel
for OCI-pulled images. Build with `scripts/make-generic-kernel.sh`.
## Adding or updating an entry
@ -50,8 +45,8 @@ The repo has no CI for kernel publishing yet. Catalog updates are manual
and infrequent (kernel version bumps every few weeks at most).
```bash
# 1. Build the kernel locally with the existing helper.
scripts/make-generic-kernel.sh # or: make void-kernel / make alpine-kernel
# 1. Build the kernel locally.
scripts/make-generic-kernel.sh
# 2. Import it into the local catalog so the canonical layout exists.
banger kernel import generic-6.12 \
@ -129,26 +124,11 @@ If hosting ever moves, catalog entries can be migrated by reuploading the
tarballs and editing the URLs in `catalog.json` — no other code changes
required.
## Tech debt: kernel-build scripts
## Tech debt
`scripts/make-void-kernel.sh` and `scripts/make-alpine-kernel.sh` are
procedural bash that fetches and patches per-distro kernel sources.
Each new distro means a new bespoke script. They're "good enough"
because catalog refreshes are infrequent and only the maintainer runs
them, but they are the bottleneck if the catalog ever wants to grow
beyond two distros.
A future iteration should:
- Move kernel acquisition into a Go (or at least uniform) tool with a
per-distro plugin/config rather than per-distro scripts.
- Encode kernel config and required modules declaratively so a Debian
or Fedora target is a config addition, not a new script.
- Run unattended in CI once banger goes public — the manual
`scripts/publish-kernel.sh` flow scales until then.
Until that happens, `make lint-shell` only runs at `--severity=error`.
Tightening to `--severity=warning` would surface real issues in the
legacy build scripts (mostly `sudo cat > file` redirects and
heredoc-quoting concerns); fixing those is a prerequisite to bumping
the lint floor.
- Kernel publishing is manual; there is no CI yet. `scripts/make-generic-kernel.sh`
plus `scripts/publish-kernel.sh` is fine while refreshes are
infrequent and maintainer-only. CI becomes relevant once banger
goes public.
- `make lint-shell` runs at `--severity=error` only. Tightening to
`--severity=warning` is a nice-to-have but low priority.

View file

@ -1,9 +0,0 @@
# 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

@ -1,9 +0,0 @@
# Experimental Void Linux guest profile for local testing.
#
# Register or promote a complete `void` 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 = "void"
# firecracker_bin = "/usr/bin/firecracker"
# ssh_key_path = "/abs/path/to/private/key"

View file

@ -75,7 +75,7 @@ RUN curl -fsSL https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh \
> /etc/profile.d/mise.sh \
&& chmod 0644 /etc/profile.d/mise.sh
# Git default branch — matches the old customize.sh opinion.
# Default branch for any git init inside the sandbox.
RUN git config --system init.defaultBranch main
# `fd-find` installs as `fdfind` on Debian to avoid a long-standing name

View file

@ -30,7 +30,6 @@ import (
"banger/internal/guest"
"banger/internal/hostnat"
"banger/internal/imagecat"
"banger/internal/imagepreset"
"banger/internal/imagepull"
"banger/internal/model"
"banger/internal/paths"
@ -219,7 +218,6 @@ func newInternalCommand() *cobra.Command {
newInternalSSHKeyPathCommand(),
newInternalFirecrackerPathCommand(),
newInternalVSockAgentPathCommand(),
newInternalPackagesCommand(),
newInternalMakeBundleCommand(),
)
return cmd
@ -284,39 +282,6 @@ func newInternalVSockAgentPathCommand() *cobra.Command {
}
}
func newInternalPackagesCommand() *cobra.Command {
var docker bool
cmd := &cobra.Command{
Use: "packages <debian|void|alpine>",
Hidden: true,
Args: exactArgsUsage(1, "usage: banger internal packages <debian|void|alpine> [--docker]"),
RunE: func(cmd *cobra.Command, args []string) error {
var packages []string
switch strings.TrimSpace(args[0]) {
case "debian":
packages = imagepreset.DebianBasePackages()
if docker {
packages = append(packages, "docker.io")
}
case "void":
packages = imagepreset.VoidBasePackages()
case "alpine":
packages = imagepreset.AlpineBasePackages()
default:
return fmt.Errorf("unknown package preset %q", args[0])
}
for _, pkg := range packages {
if _, err := fmt.Fprintln(cmd.OutOrStdout(), pkg); err != nil {
return err
}
}
return nil
},
}
cmd.Flags().BoolVar(&docker, "docker", false, "include docker-specific additions")
return cmd
}
func newInternalMakeBundleCommand() *cobra.Command {
var (
rootfsTarPath string

View file

@ -190,24 +190,6 @@ 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 TestPSAndVMListAliasesAndFlagsExist(t *testing.T) {
root := NewBangerCommand()
ps, _, err := root.Find([]string{"ps"})

View file

@ -8,14 +8,44 @@ package imagemgr
import (
"context"
"crypto/sha256"
"fmt"
"os"
"path/filepath"
"strings"
"banger/internal/imagepreset"
"banger/internal/system"
)
// debianBasePackages is the apt package list applied by
// `image build --from-image` to Debian-based managed rootfses. Small
// curated set: most of the developer tooling the golden image ships
// lives in the Dockerfile, not here.
var debianBasePackages = []string{
"make",
"git",
"less",
"tree",
"ca-certificates",
"curl",
"wget",
"iproute2",
"vim",
"tmux",
}
// DebianBasePackages returns a copy of the base package set.
func DebianBasePackages() []string {
return append([]string(nil), debianBasePackages...)
}
// hashPackages returns the hex sha256 of the package list, used as
// drift-detection metadata alongside a built rootfs.
func hashPackages(lines []string) string {
sum := sha256.Sum256([]byte(strings.Join(lines, "\n") + "\n"))
return fmt.Sprintf("%x", sum)
}
// ValidateRegisterPaths checks that rootfs + kernel exist and that optional
// artifacts, when provided, also exist.
func ValidateRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir string) error {
@ -102,7 +132,7 @@ func StageOptionalArtifactPath(artifactDir, stagedPath, name string) string {
// managed image build. The #feature:docker sentinel is appended when
// docker is requested.
func BuildMetadataPackages(docker bool) []string {
packages := imagepreset.DebianBasePackages()
packages := DebianBasePackages()
if docker {
packages = append(packages, "#feature:docker")
}
@ -116,5 +146,5 @@ func WritePackagesMetadata(rootfsPath string, packages []string) error {
return nil
}
metadataPath := rootfsPath + ".packages.sha256"
return os.WriteFile(metadataPath, []byte(imagepreset.Hash(packages)+"\n"), 0o644)
return os.WriteFile(metadataPath, []byte(hashPackages(packages)+"\n"), 0o644)
}

View file

@ -11,7 +11,6 @@ import (
"banger/internal/api"
"banger/internal/daemon/imagemgr"
"banger/internal/imagepreset"
"banger/internal/kernelcat"
"banger/internal/model"
"banger/internal/system"
@ -86,7 +85,7 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
if err != nil {
return model.Image{}, err
}
packages := imagepreset.DebianBasePackages()
packages := imagemgr.DebianBasePackages()
metadataPackages := imagemgr.BuildMetadataPackages(params.Docker)
spec := imageBuildSpec{
ID: id,

View file

@ -1,86 +0,0 @@
package imagepreset
import (
"crypto/sha256"
"fmt"
"strings"
)
var debianBase = []string{
"make",
"git",
"less",
"tree",
"ca-certificates",
"curl",
"wget",
"iproute2",
"vim",
"tmux",
}
var voidBase = []string{
"base-minimal",
"base-devel",
"bash",
"ca-certificates",
"curl",
"docker",
"docker-compose",
"e2fsprogs",
"git",
"iproute2",
"less",
"make",
"openssh",
"procps-ng",
"runit",
"shadow",
"sudo",
"tmux",
"tree",
"vim",
"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...)
}
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)
}

View file

@ -10,16 +10,6 @@
"tarball_sha256": "d6f9ba2a957260063241cf9d79ae538d0c349107d37f0bfccc33281d29bd0901",
"size_bytes": 9098722,
"description": "Generic Firecracker kernel 6.12.8 (all drivers built-in, no initrd needed)"
},
{
"name": "void-6.12",
"distro": "void",
"arch": "x86_64",
"kernel_version": "6.12.81_1",
"tarball_url": "https://kernels.thaloco.com/void-6.12-x86_64.tar.zst",
"tarball_sha256": "3de6d03c4a3b5d3b8164f20049ddcb38b32a1864ea7133f01ff7fbb56c34d428",
"size_bytes": 187734807,
"description": "Void Linux 6.12 kernel for Firecracker microVMs"
}
]
}

View file

@ -18,8 +18,9 @@ type DiscoveredArtifacts struct {
ModulesDir string
}
// metadataFile is the JSON dropped by scripts/make-void-kernel.sh alongside
// its staged output. We read it when present to avoid guessing at filenames.
// metadataFile is the optional JSON a kernel-build script can drop
// alongside its staged output to point ReadLocal at specific filenames
// without guessing.
type metadataFile struct {
KernelPath string `json:"kernel_path"`
InitrdPath string `json:"initrd_path"`

View file

@ -1,597 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[customize] %s\n' "$*"
}
usage() {
cat <<'EOF'
Usage: ./scripts/customize.sh <base-rootfs> [--out <path>] [--size <size>] [--kernel <path>] [--initrd <path>] [--docker] [--modules <dir>]
Creates a copy of rootfs.ext4, optionally resizes it, boots a VM using the
copy as a writable rootfs, then applies base configuration and packages.
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) echo $((num * 1024)) ;;
M|"") echo $((num * 1024 * 1024)) ;;
G) echo $((num * 1024 * 1024 * 1024)) ;;
esac
return 0
fi
return 1
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/image-build}"
VM_ROOT="$STATE/vms"
mkdir -p "$VM_ROOT"
BR_DEV="br-fc"
BR_IP="172.16.0.1"
CIDR="24"
DNS_SERVER="1.1.1.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; install/build banger or set BANGER_BIN"
exit 1
}
BANGER_BIN="$(resolve_banger_bin)"
NAT_ACTIVE=0
FC_BIN="$("$BANGER_BIN" internal firecracker-path)"
SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)"
VSOCK_AGENT="$("$BANGER_BIN" internal vsock-agent-path)"
banger_nat() {
local action="$1"
"$BANGER_BIN" internal nat "$action" --guest-ip "$GUEST_IP" --tap "$TAP_DEV"
}
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"
}
BASE_ROOTFS=""
OUT_ROOTFS=""
SIZE_SPEC=""
INSTALL_DOCKER=0
KERNEL=""
INITRD=""
MISE_VERSION="v2025.12.0"
MISE_INSTALL_PATH="/usr/local/bin/mise"
MISE_ACTIVATE_LINE='eval "$(/usr/local/bin/mise activate bash)"'
NODE_TOOL="node@22"
CLAUDE_CODE_TOOL="npm:@anthropic-ai/claude-code"
PI_TOOL="npm:@mariozechner/pi-coding-agent"
TMUX_PLUGIN_DIR="/root/.tmux/plugins"
TMUX_RESURRECT_DIR="/root/.tmux/resurrect"
TMUX_TPM_REPO="https://github.com/tmux-plugins/tpm"
TMUX_RESURRECT_REPO="https://github.com/tmux-plugins/tmux-resurrect"
TMUX_CONTINUUM_REPO="https://github.com/tmux-plugins/tmux-continuum"
TMUX_MANAGED_START="# >>> banger tmux plugins >>>"
TMUX_MANAGED_END="# <<< banger tmux plugins <<<"
MODULES_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--out)
OUT_ROOTFS="${2:-}"
shift 2
;;
--size)
SIZE_SPEC="${2:-}"
shift 2
;;
--kernel)
KERNEL="${2:-}"
shift 2
;;
--initrd)
INITRD="${2:-}"
shift 2
;;
--docker)
INSTALL_DOCKER=1
shift
;;
--modules)
MODULES_DIR="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
if [[ -z "$BASE_ROOTFS" ]]; then
BASE_ROOTFS="$1"
shift
else
log "unknown option: $1"
usage
exit 1
fi
;;
esac
done
if [[ -z "$BASE_ROOTFS" ]]; then
usage
exit 1
fi
if [[ ! -f "$BASE_ROOTFS" ]]; then
log "base rootfs not found: $BASE_ROOTFS"
exit 1
fi
if [[ -z "$OUT_ROOTFS" ]]; then
base_dir="$(dirname "$BASE_ROOTFS")"
base_name="$(basename "$BASE_ROOTFS")"
OUT_ROOTFS="${base_dir}/docker-${base_name}"
fi
if [[ "$OUT_ROOTFS" == *.ext4 ]]; then
WORK_SEED="${OUT_ROOTFS%.ext4}.work-seed.ext4"
else
WORK_SEED="${OUT_ROOTFS}.work-seed"
fi
if [[ -z "$KERNEL" ]]; then
log "kernel path is required; pass --kernel"
exit 1
fi
if [[ ! -f "$KERNEL" ]]; then
log "kernel not found: $KERNEL"
exit 1
fi
if [[ -n "$INITRD" && ! -f "$INITRD" ]]; then
log "initrd not found: $INITRD"
exit 1
fi
if [[ -n "$MODULES_DIR" && ! -d "$MODULES_DIR" ]]; then
log "modules dir not found: $MODULES_DIR"
exit 1
fi
if [[ -e "$OUT_ROOTFS" ]]; then
log "output rootfs already exists: $OUT_ROOTFS"
exit 1
fi
if ! command -v resize2fs >/dev/null 2>&1; then
log "resize2fs required"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
log "jq required"
exit 1
fi
if ! command -v sha256sum >/dev/null 2>&1; then
log "sha256sum required to record package preset metadata"
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
APT_PACKAGES=()
if ! load_package_preset debian APT_PACKAGES; then
log "debian package preset is empty"
exit 1
fi
if ! PACKAGES_HASH="$(printf '%s\n' "${APT_PACKAGES[@]}" | sha256sum | awk '{print $1}')"; then
log "failed to hash package preset"
exit 1
fi
printf -v APT_PACKAGES_ESCAPED '%q ' "${APT_PACKAGES[@]}"
log "copying base rootfs to $OUT_ROOTFS"
cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS"
if [[ -n "$SIZE_SPEC" ]]; then
SIZE_BYTES="$(parse_size "$SIZE_SPEC")"
BASE_BYTES="$(stat -c%s "$BASE_ROOTFS")"
if [[ -z "$SIZE_BYTES" || "$SIZE_BYTES" -lt "$BASE_BYTES" ]]; then
log "size must be >= base image size"
exit 1
fi
log "resizing rootfs to $SIZE_SPEC"
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
e2fsck -p -f "$OUT_ROOTFS" >/dev/null
resize2fs "$OUT_ROOTFS" >/dev/null
fi
VM_ID="$(head -c 32 /dev/urandom | xxd -p -c 256)"
VM_TAG="${VM_ID:0:8}"
VM_NAME="customize-${VM_TAG}"
VM_DIR="$VM_ROOT/$VM_ID"
mkdir -p "$VM_DIR"
API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock"
LOG_FILE="$VM_DIR/firecracker.log"
TAP_DEV="tap-fc-$VM_TAG"
# Allocate guest IP
NEXT_IP_FILE="$STATE/next_ip"
NEXT_IP="$(cat "$NEXT_IP_FILE" 2>/dev/null || echo 2)"
GUEST_IP="172.16.0.$NEXT_IP"
echo "$((NEXT_IP + 1))" > "$NEXT_IP_FILE"
sudo -v
cleanup() {
sudo kill "${FC_PID:-}" 2>/dev/null || true
if [[ "$NAT_ACTIVE" -eq 1 ]]; then
banger_nat down >/dev/null 2>&1 || true
fi
sudo ip link del "$TAP_DEV" 2>/dev/null || true
rm -f "$API_SOCK"
rm -rf "$VM_DIR"
}
trap cleanup EXIT
sudo mkdir -p "$(dirname "$API_SOCK")"
sudo chown "$(id -u):$(id -g)" "$(dirname "$API_SOCK")"
# Host bridge
if ! ip link show "$BR_DEV" >/dev/null 2>&1; then
log "creating host bridge $BR_DEV ($BR_IP/$CIDR)"
sudo ip link add name "$BR_DEV" type bridge
sudo ip addr add "${BR_IP}/${CIDR}" dev "$BR_DEV"
sudo ip link set "$BR_DEV" up
else
sudo ip link set "$BR_DEV" up
fi
log "creating tap device $TAP_DEV"
TAP_USER="${SUDO_UID:-$(id -u)}"
TAP_GROUP="${SUDO_GID:-$(id -g)}"
sudo ip tuntap add dev "$TAP_DEV" mode tap user "$TAP_USER" group "$TAP_GROUP"
sudo ip link set "$TAP_DEV" master "$BR_DEV"
sudo ip link set "$TAP_DEV" up
sudo ip link set "$BR_DEV" up
log "starting firecracker process"
rm -f "$API_SOCK"
nohup sudo -E "$FC_BIN" --api-sock "$API_SOCK" >"$LOG_FILE" 2>&1 &
FC_PID="$!"
log "waiting for firecracker api socket"
for _ in $(seq 1 200); do
[[ -S "$API_SOCK" ]] && break
sleep 0.02
done
[[ -S "$API_SOCK" ]] || { log "firecracker api socket not ready"; exit 1; }
log "configuring machine"
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \
-H "Content-Type: application/json" \
-d '{
"vcpu_count": 2,
"mem_size_mib": 1024,
"smt": false
}' >/dev/null
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
INITRD_JSON=", \"initrd_path\": \"$INITRD\""
fi
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \
-H "Content-Type: application/json" \
-d "{
\"kernel_image_path\": \"$KERNEL\",
\"boot_args\": \"$KCMD\"${INITRD_JSON}
}" >/dev/null
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/rootfs \
-H "Content-Type: application/json" \
-d "{
\"drive_id\": \"rootfs\",
\"path_on_host\": \"$OUT_ROOTFS\",
\"is_root_device\": true,
\"is_read_only\": false
}" >/dev/null
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/network-interfaces/eth0 \
-H "Content-Type: application/json" \
-d "{
\"iface_id\": \"eth0\",
\"host_dev_name\": \"$TAP_DEV\"
}" >/dev/null
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \
-H "Content-Type: application/json" \
-d '{ "action_type": "InstanceStart" }' >/dev/null
SUDO_CHILD_PID="$(pgrep -n -f "$API_SOCK" || true)"
if [[ -n "$SUDO_CHILD_PID" ]]; then
FC_PID="$SUDO_CHILD_PID"
fi
VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
CREATED_AT="$(date -Iseconds)"
jq -n \
--arg id "$VM_ID" \
--arg name "$VM_NAME" \
--arg pid "$FC_PID" \
--arg created_at "$CREATED_AT" \
--arg guest_ip "$GUEST_IP" \
--arg tap "$TAP_DEV" \
--arg api_sock "$API_SOCK" \
--arg log "$LOG_FILE" \
--arg rootfs "$OUT_ROOTFS" \
--arg kernel "$KERNEL" \
--argjson config "$VM_CONFIG_JSON" \
'{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel},config:$config}' \
> "$VM_DIR/vm.json"
log "enabling NAT for customization"
banger_nat up >/dev/null
NAT_ACTIVE=1
log "waiting for SSH"
SSH_READY=0
for _ in $(seq 1 60); do
if ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"root@${GUEST_IP}" "true" >/dev/null 2>&1; then
SSH_READY=1
break
fi
sleep 1
done
if [[ "$SSH_READY" -ne 1 ]]; then
log "ssh did not become ready on $GUEST_IP"
exit 1
fi
log "configuring guest"
log "installing vsock agent"
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"$VSOCK_AGENT" "root@${GUEST_IP}:/usr/local/bin/banger-vsock-agent" >/dev/null
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"root@${GUEST_IP}" bash -lc "set -e
printf 'nameserver %s\n' \"$DNS_SERVER\" > /etc/resolv.conf
echo \"$VM_NAME\" > /etc/hostname
printf '127.0.0.1 localhost\n127.0.1.1 %s\n' \"$VM_NAME\" > /etc/hosts
touch /etc/fstab
sed -i '\|^/dev/vdb[[:space:]]\+/home[[:space:]]|d; \|^/dev/vdc[[:space:]]\+/var[[:space:]]|d' /etc/fstab
if ! grep -q '^tmpfs /run ' /etc/fstab; then
echo 'tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0' >> /etc/fstab
fi
if ! grep -q '^tmpfs /tmp ' /etc/fstab; then
echo 'tmpfs /tmp tmpfs defaults,nodev,nosuid,mode=1777 0 0' >> /etc/fstab
fi
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
DEBIAN_FRONTEND=noninteractive apt-get -y install ${APT_PACKAGES_ESCAPED}
curl -fsSL https://mise.run | MISE_INSTALL_PATH="$MISE_INSTALL_PATH" MISE_VERSION="$MISE_VERSION" sh
"$MISE_INSTALL_PATH" use -g "$NODE_TOOL"
"$MISE_INSTALL_PATH" use -g github:anomalyco/opencode
"$MISE_INSTALL_PATH" use -g "$CLAUDE_CODE_TOOL"
"$MISE_INSTALL_PATH" use -g "$PI_TOOL"
"$MISE_INSTALL_PATH" reshim
if [[ ! -e /root/.local/share/mise/shims/node ]]; then
echo 'node shim not found after mise install' >&2
exit 1
fi
if [[ ! -e /root/.local/share/mise/shims/npm ]]; then
echo 'npm shim not found after mise install' >&2
exit 1
fi
if [[ ! -e /root/.local/share/mise/shims/opencode ]]; then
echo 'opencode shim not found after mise install' >&2
exit 1
fi
if [[ ! -e /root/.local/share/mise/shims/claude ]]; then
echo 'claude shim not found after mise install' >&2
exit 1
fi
if [[ ! -e /root/.local/share/mise/shims/pi ]]; then
echo 'pi shim not found after mise install' >&2
exit 1
fi
ln -snf /root/.local/share/mise/shims/node /usr/local/bin/node
ln -snf /root/.local/share/mise/shims/npm /usr/local/bin/npm
ln -snf /root/.local/share/mise/shims/opencode /usr/local/bin/opencode
ln -snf /root/.local/share/mise/shims/claude /usr/local/bin/claude
ln -snf /root/.local/share/mise/shims/pi /usr/local/bin/pi
mkdir -p /etc/profile.d
cat > /etc/profile.d/mise.sh <<'MISEPROFILE'
if [ -n \"\${BASH_VERSION:-}\" ] && [ -x \"$MISE_INSTALL_PATH\" ]; then
eval \"\$($MISE_INSTALL_PATH activate bash)\"
fi
MISEPROFILE
chmod 0644 /etc/profile.d/mise.sh
touch /etc/bash.bashrc
if ! grep -Fqx '$MISE_ACTIVATE_LINE' /etc/bash.bashrc; then
printf '\n%s\n' '$MISE_ACTIVATE_LINE' >> /etc/bash.bashrc
fi
if [[ \"$INSTALL_DOCKER\" == \"1\" ]]; then
DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true
if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then
DEBIAN_FRONTEND=noninteractive apt-get -y install docker.io
fi
if command -v systemctl >/dev/null 2>&1; then
systemctl enable --now docker || true
fi
fi
rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh
chmod 0755 /usr/local/bin/banger-vsock-agent
mkdir -p /etc/modules-load.d /etc/systemd/system
cat > /etc/systemd/system/banger-opencode.service <<'EOF'
[Unit]
Description=Banger opencode server
After=network.target
RequiresMountsFor=/root
[Service]
Type=simple
Environment=HOME=/root
WorkingDirectory=/root
ExecStart=/usr/local/bin/opencode serve --hostname 0.0.0.0 --port 4096
Restart=on-failure
RestartSec=1
[Install]
WantedBy=multi-user.target
EOF
chmod 0644 /etc/systemd/system/banger-opencode.service
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload || true
systemctl enable --now banger-opencode.service || true
fi
cat > /etc/modules-load.d/banger-vsock.conf <<'EOF'
vsock
vmw_vsock_virtio_transport
EOF
chmod 0644 /etc/modules-load.d/banger-vsock.conf
cat > /etc/systemd/system/banger-vsock-agent.service <<'EOF'
[Unit]
Description=Banger vsock agent
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/banger-vsock-agent
Restart=on-failure
RestartSec=1
[Install]
WantedBy=multi-user.target
EOF
chmod 0644 /etc/systemd/system/banger-vsock-agent.service
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload || true
systemctl enable --now banger-vsock-agent.service || true
fi
git config --system init.defaultBranch main
"
log "configuring tmux resurrect"
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"root@${GUEST_IP}" bash -se <<EOF
set -euo pipefail
install_tmux_plugin() {
local dir="\$1"
local repo="\$2"
if [[ -d "\$dir/.git" ]]; then
git -C "\$dir" fetch --depth 1 origin
git -C "\$dir" reset --hard FETCH_HEAD
else
rm -rf "\$dir"
git clone --depth 1 "\$repo" "\$dir"
fi
}
mkdir -p "$TMUX_PLUGIN_DIR" "$TMUX_RESURRECT_DIR"
install_tmux_plugin "$TMUX_PLUGIN_DIR/tpm" "$TMUX_TPM_REPO"
install_tmux_plugin "$TMUX_PLUGIN_DIR/tmux-resurrect" "$TMUX_RESURRECT_REPO"
install_tmux_plugin "$TMUX_PLUGIN_DIR/tmux-continuum" "$TMUX_CONTINUUM_REPO"
TMUX_CONF="/root/.tmux.conf"
tmp_tmux_conf="\$(mktemp)"
if [[ -f "\$TMUX_CONF" ]]; then
awk -v begin="$TMUX_MANAGED_START" -v end="$TMUX_MANAGED_END" '
\$0 == begin { skip = 1; next }
\$0 == end { skip = 0; next }
!skip { print }
' "\$TMUX_CONF" > "\$tmp_tmux_conf"
else
: > "\$tmp_tmux_conf"
fi
if [[ -s "\$tmp_tmux_conf" ]]; then
printf '\n' >> "\$tmp_tmux_conf"
fi
cat >> "\$tmp_tmux_conf" <<'TMUXCONF'
$TMUX_MANAGED_START
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @continuum-save-interval '15'
set -g @continuum-restore 'off'
set -g @resurrect-dir '/root/.tmux/resurrect'
run '~/.tmux/plugins/tpm/tpm'
$TMUX_MANAGED_END
TMUXCONF
mv "\$tmp_tmux_conf" "\$TMUX_CONF"
chmod 0644 "\$TMUX_CONF"
EOF
if [[ -n "$MODULES_DIR" ]]; then
MODULES_BASE="$(basename "$MODULES_DIR")"
log "copying kernel modules ($MODULES_BASE) into guest"
tar -C "$(dirname "$MODULES_DIR")" -cf - "$MODULES_BASE" | \
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"root@${GUEST_IP}" bash -lc "set -e
mkdir -p /lib/modules
tar -C /lib/modules -xf -
depmod -a \"$MODULES_BASE\"
mkdir -p /etc/modules-load.d
printf 'nf_tables\nnft_chain_nat\nveth\nbr_netfilter\noverlay\n' > /etc/modules-load.d/docker-netfilter.conf
mkdir -p /etc/sysctl.d
cat > /etc/sysctl.d/99-docker.conf <<'SYSCTL'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
SYSCTL
sysctl --system >/dev/null 2>&1 || true
sync
"
fi
log "shutting down guest"
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"root@${GUEST_IP}" bash -lc "sync" || true
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \
-H "Content-Type: application/json" \
-d '{ "action_type": "SendCtrlAltDel" }' >/dev/null || true
for _ in $(seq 1 200); do
if ! ps -p "$FC_PID" >/dev/null 2>&1; then
break
fi
sleep 0.05
done
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"
log "done"

View file

@ -1,306 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[interactive] %s\n' "$*"
}
usage() {
cat <<'EOF'
Usage: ./scripts/interactive.sh <base-rootfs> --kernel <path> [--initrd <path>] [--size <size>]
Creates a writable copy of the base rootfs and boots a VM so you can
customize it manually over SSH. No automatic package/config changes
are applied.
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) echo $((num * 1024)) ;;
M|"") echo $((num * 1024 * 1024)) ;;
G) echo $((num * 1024 * 1024 * 1024)) ;;
esac
return 0
fi
return 1
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/interactive}"
VM_ROOT="$STATE/vms"
mkdir -p "$VM_ROOT"
BR_DEV="br-fc"
BR_IP="172.16.0.1"
CIDR="24"
DNS_SERVER="1.1.1.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; install/build banger or set BANGER_BIN"
exit 1
}
BANGER_BIN="$(resolve_banger_bin)"
NAT_ACTIVE=0
FC_BIN="$("$BANGER_BIN" internal firecracker-path)"
SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)"
KERNEL=""
INITRD=""
banger_nat() {
local action="$1"
"$BANGER_BIN" internal nat "$action" --guest-ip "$GUEST_IP" --tap "$TAP_DEV"
}
BASE_ROOTFS=""
OUT_ROOTFS=""
SIZE_SPEC=""
while [[ $# -gt 0 ]]; do
case "$1" in
--out)
OUT_ROOTFS="${2:-}"
shift 2
;;
--size)
SIZE_SPEC="${2:-}"
shift 2
;;
--kernel)
KERNEL="${2:-}"
shift 2
;;
--initrd)
INITRD="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
if [[ -z "$BASE_ROOTFS" ]]; then
BASE_ROOTFS="$1"
shift
else
log "unknown option: $1"
usage
exit 1
fi
;;
esac
done
if [[ -z "$BASE_ROOTFS" ]]; then
usage
exit 1
fi
if [[ ! -f "$BASE_ROOTFS" ]]; then
log "base rootfs not found: $BASE_ROOTFS"
exit 1
fi
if [[ -z "$KERNEL" ]]; then
log "kernel path is required; pass --kernel"
exit 1
fi
if [[ ! -f "$KERNEL" ]]; then
log "kernel not found: $KERNEL"
exit 1
fi
if [[ -n "$INITRD" && ! -f "$INITRD" ]]; then
log "initrd not found: $INITRD"
exit 1
fi
if [[ -z "$OUT_ROOTFS" ]]; then
base_dir="$(dirname "$BASE_ROOTFS")"
base_name="$(basename "$BASE_ROOTFS")"
OUT_ROOTFS="${base_dir}/rw-${base_name}"
fi
if [[ -e "$OUT_ROOTFS" ]]; then
log "output rootfs already exists: $OUT_ROOTFS"
exit 1
fi
log "copying base rootfs to $OUT_ROOTFS"
cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS"
if [[ -n "$SIZE_SPEC" ]]; then
SIZE_BYTES="$(parse_size "$SIZE_SPEC")"
BASE_BYTES="$(stat -c%s "$BASE_ROOTFS")"
if [[ -z "$SIZE_BYTES" || "$SIZE_BYTES" -lt "$BASE_BYTES" ]]; then
log "size must be >= base image size"
exit 1
fi
log "resizing rootfs to $SIZE_SPEC"
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
e2fsck -p -f "$OUT_ROOTFS" >/dev/null
resize2fs "$OUT_ROOTFS" >/dev/null
fi
VM_ID="$(head -c 32 /dev/urandom | xxd -p -c 256)"
VM_TAG="${VM_ID:0:8}"
VM_NAME="interactive-${VM_TAG}"
VM_DIR="$VM_ROOT/$VM_ID"
mkdir -p "$VM_DIR"
API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock"
LOG_FILE="$VM_DIR/firecracker.log"
TAP_DEV="tap-fc-$VM_TAG"
# Allocate guest IP
NEXT_IP_FILE="$STATE/next_ip"
NEXT_IP="$(cat "$NEXT_IP_FILE" 2>/dev/null || echo 2)"
GUEST_IP="172.16.0.$NEXT_IP"
echo "$((NEXT_IP + 1))" > "$NEXT_IP_FILE"
sudo -v
cleanup() {
sudo kill "${FC_PID:-}" 2>/dev/null || true
if [[ "$NAT_ACTIVE" -eq 1 ]]; then
banger_nat down >/dev/null 2>&1 || true
fi
sudo ip link del "$TAP_DEV" 2>/dev/null || true
rm -f "$API_SOCK"
rm -rf "$VM_DIR"
}
trap cleanup EXIT
sudo mkdir -p "$(dirname "$API_SOCK")"
sudo chown "$(id -u):$(id -g)" "$(dirname "$API_SOCK")"
# Host bridge
if ! ip link show "$BR_DEV" >/dev/null 2>&1; then
log "creating host bridge $BR_DEV ($BR_IP/$CIDR)"
sudo ip link add name "$BR_DEV" type bridge
sudo ip addr add "${BR_IP}/${CIDR}" dev "$BR_DEV"
sudo ip link set "$BR_DEV" up
else
sudo ip link set "$BR_DEV" up
fi
log "creating tap device $TAP_DEV"
TAP_USER="${SUDO_UID:-$(id -u)}"
TAP_GROUP="${SUDO_GID:-$(id -g)}"
sudo ip tuntap add dev "$TAP_DEV" mode tap user "$TAP_USER" group "$TAP_GROUP"
sudo ip link set "$TAP_DEV" master "$BR_DEV"
sudo ip link set "$TAP_DEV" up
sudo ip link set "$BR_DEV" up
log "starting firecracker process"
rm -f "$API_SOCK"
nohup sudo -E "$FC_BIN" --api-sock "$API_SOCK" >"$LOG_FILE" 2>&1 &
FC_PID="$!"
log "waiting for firecracker api socket"
for _ in $(seq 1 200); do
[[ -S "$API_SOCK" ]] && break
sleep 0.02
done
[[ -S "$API_SOCK" ]] || { log "firecracker api socket not ready"; exit 1; }
log "configuring machine"
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \
-H "Content-Type: application/json" \
-d '{
"vcpu_count": 2,
"mem_size_mib": 1024,
"smt": false
}' >/dev/null
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" \
-d "{
\"kernel_image_path\": \"$KERNEL\",
\"boot_args\": \"$KCMD\",
\"initrd_path\": \"$INITRD\"
}" >/dev/null
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/rootfs \
-H "Content-Type: application/json" \
-d "{
\"drive_id\": \"rootfs\",
\"path_on_host\": \"$OUT_ROOTFS\",
\"is_root_device\": true,
\"is_read_only\": false
}" >/dev/null
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/network-interfaces/eth0 \
-H "Content-Type: application/json" \
-d "{
\"iface_id\": \"eth0\",
\"host_dev_name\": \"$TAP_DEV\"
}" >/dev/null
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \
-H "Content-Type: application/json" \
-d '{ "action_type": "InstanceStart" }' >/dev/null
SUDO_CHILD_PID="$(pgrep -n -f "$API_SOCK" || true)"
if [[ -n "$SUDO_CHILD_PID" ]]; then
FC_PID="$SUDO_CHILD_PID"
fi
VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
CREATED_AT="$(date -Iseconds)"
jq -n \
--arg id "$VM_ID" \
--arg name "$VM_NAME" \
--arg pid "$FC_PID" \
--arg created_at "$CREATED_AT" \
--arg guest_ip "$GUEST_IP" \
--arg tap "$TAP_DEV" \
--arg api_sock "$API_SOCK" \
--arg log "$LOG_FILE" \
--arg rootfs "$OUT_ROOTFS" \
--arg kernel "$KERNEL" \
--argjson config "$VM_CONFIG_JSON" \
'{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel},config:$config}' \
> "$VM_DIR/vm.json"
log "enabling NAT for interactive session"
banger_nat up >/dev/null
NAT_ACTIVE=1
log "waiting for SSH"
log "guest ip: $GUEST_IP"
log "ssh: ssh -i \"$SSH_KEY\" root@${GUEST_IP}"
for _ in $(seq 1 60); do
if ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"root@${GUEST_IP}" "true" >/dev/null 2>&1; then
log "ssh ready"
break
fi
sleep 1
done
log "output rootfs: $OUT_ROOTFS"
log "press Ctrl+C to stop and clean up"
while kill -0 "$FC_PID" >/dev/null 2>&1; do
sleep 1
done

View file

@ -1,363 +0,0 @@
#!/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"

View file

@ -1,722 +0,0 @@
#!/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"

View file

@ -1,616 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[make-rootfs-void] %s\n' "$*"
}
usage() {
cat <<'EOF'
Usage: ./scripts/make-rootfs-void.sh [--out <path>] [--size <size>] [--mirror <url>] [--arch <arch>]
Build an experimental Void Linux rootfs image plus a matching /root work-seed.
Defaults:
--out ./build/manual/rootfs-void.ext4
--size 4G
--mirror https://repo-default.voidlinux.org
--arch x86_64
This path is experimental and local-only. If ./build/manual/void-kernel exists
it uses the staged Void 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
}
normalize_mirror() {
local mirror="${1%/}"
mirror="${mirror%/current}"
mirror="${mirror%/static}"
printf '%s\n' "$mirror"
}
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
}
find_static_binary() {
local name="$1"
find "$STATIC_DIR" -type f \( -name "$name" -o -name "$name.static" \) -perm -u+x | sort | head -n 1
}
find_static_keys_dir() {
find "$STATIC_DIR" -type d -path '*/var/db/xbps/keys' | sort | head -n 1
}
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
}
install_vsock_service() {
local service_dir="$ROOT_MOUNT/etc/sv/banger-vsock-agent"
local run_path="$service_dir/run"
local finish_path="$service_dir/finish"
sudo mkdir -p "$service_dir"
cat <<'EOF' | sudo tee "$run_path" >/dev/null
#!/bin/sh
modprobe vsock 2>/dev/null || true
modprobe vmw_vsock_virtio_transport 2>/dev/null || true
exec /usr/local/bin/banger-vsock-agent
EOF
cat <<'EOF' | sudo tee "$finish_path" >/dev/null
#!/bin/sh
exit 0
EOF
sudo chmod 0755 "$run_path" "$finish_path"
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
sudo ln -snf /etc/sv/banger-vsock-agent "$ROOT_MOUNT/etc/runit/runsvdir/default/banger-vsock-agent"
}
install_opencode_service() {
local service_dir="$ROOT_MOUNT/etc/sv/banger-opencode"
local run_path="$service_dir/run"
local finish_path="$service_dir/finish"
sudo mkdir -p "$service_dir"
cat <<'EOF' | sudo tee "$run_path" >/dev/null
#!/bin/sh
set -e
export HOME=/root
cd /root
exec /usr/local/bin/opencode serve --hostname 0.0.0.0 --port 4096
EOF
cat <<'EOF' | sudo tee "$finish_path" >/dev/null
#!/bin/sh
exit 0
EOF
sudo chmod 0755 "$run_path" "$finish_path"
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
sudo ln -snf /etc/sv/banger-opencode "$ROOT_MOUNT/etc/runit/runsvdir/default/banger-opencode"
}
install_guest_network_bootstrap() {
sudo mkdir -p "$ROOT_MOUNT/usr/local/libexec" "$ROOT_MOUNT/etc/runit/core-services"
sudo install -m 0755 "$GUESTNET_BOOTSTRAP_SCRIPT" "$ROOT_MOUNT/usr/local/libexec/banger-network-bootstrap"
sudo install -m 0644 "$GUESTNET_VOID_CORE_SERVICE" "$ROOT_MOUNT/etc/runit/core-services/20-banger-network.sh"
}
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"
local service_dir="$ROOT_MOUNT/etc/sv/docker"
local run_path="$service_dir/run"
local orig_run_path="$service_dir/run.orig"
local preflight_path="$ROOT_MOUNT/usr/local/bin/banger-docker-preflight"
sudo mkdir -p "$ROOT_MOUNT/etc/modules-load.d" "$ROOT_MOUNT/etc/sysctl.d" "$ROOT_MOUNT/usr/local/bin"
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
cat <<'EOF' | sudo tee "$preflight_path" >/dev/null
#!/bin/sh
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 --load /etc/sysctl.d/99-docker.conf >/dev/null 2>&1 || true
fi
EOF
if [[ ! -f "$run_path" ]]; then
log "Void rootfs is missing /etc/sv/docker/run after docker install"
exit 1
fi
sudo install -m 0755 "$run_path" "$orig_run_path"
cat <<'EOF' | sudo tee "$run_path" >/dev/null
#!/bin/sh
set -e
/usr/local/bin/banger-docker-preflight
exec /etc/sv/docker/run.orig
EOF
sudo chmod 0644 "$modules_conf" "$sysctl_conf"
sudo chmod 0755 "$preflight_path" "$run_path" "$orig_run_path"
}
enable_sshd_service() {
if [[ ! -d "$ROOT_MOUNT/etc/sv/sshd" ]]; then
log "Void rootfs is missing /etc/sv/sshd after openssh install"
exit 1
fi
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
sudo ln -snf /etc/sv/sshd "$ROOT_MOUNT/etc/runit/runsvdir/default/sshd"
}
enable_docker_service() {
if [[ ! -d "$ROOT_MOUNT/etc/sv/docker" ]]; then
log "Void rootfs is missing /etc/sv/docker after docker install"
exit 1
fi
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
sudo ln -snf /etc/sv/docker "$ROOT_MOUNT/etc/runit/runsvdir/default/docker"
}
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 Void image: $wanted_shell"
exit 1
fi
if [[ ! -f "$shells" ]]; then
log "Void image is missing /etc/shells"
exit 1
fi
if ! sudo grep -Fxq "$wanted_shell" "$shells"; then
log "Void 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 Void 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_tools() {
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 chroot "$ROOT_MOUNT" /bin/bash -se <<EOF
set -euo pipefail
curl -fsSL https://mise.run | MISE_INSTALL_PATH="$MISE_INSTALL_PATH" MISE_VERSION="$MISE_VERSION" sh
"$MISE_INSTALL_PATH" use -g "$NODE_TOOL"
"$MISE_INSTALL_PATH" use -g "$OPENCODE_TOOL"
"$MISE_INSTALL_PATH" use -g "$CLAUDE_CODE_TOOL"
"$MISE_INSTALL_PATH" use -g "$PI_TOOL"
"$MISE_INSTALL_PATH" reshim
if [[ ! -e /root/.local/share/mise/shims/node ]]; then
echo "node shim not found after mise install" >&2
exit 1
fi
if [[ ! -e /root/.local/share/mise/shims/npm ]]; then
echo "npm shim not found after mise install" >&2
exit 1
fi
if [[ ! -e /root/.local/share/mise/shims/opencode ]]; then
echo "opencode shim not found after mise install" >&2
exit 1
fi
if [[ ! -e /root/.local/share/mise/shims/claude ]]; then
echo "claude shim not found after mise install" >&2
exit 1
fi
if [[ ! -e /root/.local/share/mise/shims/pi ]]; then
echo "pi shim not found after mise install" >&2
exit 1
fi
ln -snf /root/.local/share/mise/shims/node /usr/local/bin/node
ln -snf /root/.local/share/mise/shims/npm /usr/local/bin/npm
ln -snf /root/.local/share/mise/shims/opencode /usr/local/bin/opencode
ln -snf /root/.local/share/mise/shims/claude /usr/local/bin/claude
ln -snf /root/.local/share/mise/shims/pi /usr/local/bin/pi
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"
}
cleanup() {
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-void.ext4"
SIZE_SPEC="4G"
MIRROR="https://repo-default.voidlinux.org"
ARCH="x86_64"
MISE_VERSION="v2025.12.0"
MISE_INSTALL_PATH="/usr/local/bin/mise"
NODE_TOOL="node@22"
OPENCODE_TOOL="github:anomalyco/opencode"
CLAUDE_CODE_TOOL="npm:@anthropic-ai/claude-code"
PI_TOOL="npm:@mariozechner/pi-coding-agent"
GUESTNET_BOOTSTRAP_SCRIPT="$REPO_ROOT/internal/guestnet/assets/bootstrap.sh"
GUESTNET_VOID_CORE_SERVICE="$REPO_ROOT/internal/guestnet/assets/void-core-service.sh"
MODULES_DIR=""
VOID_KERNEL_MODULES_DIR="$(find_latest_module_dir "$MANUAL_DIR/void-kernel/lib/modules" || true)"
VSOCK_AGENT="$("$BANGER_BIN" internal vsock-agent-path)"
if [[ -n "$VOID_KERNEL_MODULES_DIR" ]]; then
MODULES_DIR="$VOID_KERNEL_MODULES_DIR"
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--out)
OUT_ROOTFS="${2:-}"
shift 2
;;
--size)
SIZE_SPEC="${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
MIRROR="$(normalize_mirror "$MIRROR")"
REPO_URL="$MIRROR/current"
STATIC_ARCHIVE_URL="$MIRROR/static/xbps-static-latest.x86_64-musl.tar.xz"
if [[ "$ARCH" != "x86_64" ]]; then
log "unsupported arch: $ARCH"
log "this experimental builder currently supports only x86_64-glibc"
exit 1
fi
if [[ -z "$MODULES_DIR" || ! -d "$MODULES_DIR" ]]; then
log "modules dir not found; run 'make void-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 [[ ! -f "$GUESTNET_VOID_CORE_SERVICE" ]]; then
log "guest network core-service shim not found: $GUESTNET_VOID_CORE_SERVICE"
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
VOID_PACKAGES=()
if ! load_package_preset void VOID_PACKAGES; then
log "void package preset is empty"
exit 1
fi
if ! PACKAGES_HASH="$(printf '%s\n' "${VOID_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
TMP_DIR="$(mktemp -d -t banger-void-rootfs-XXXXXX)"
STATIC_DIR="$TMP_DIR/static"
ROOT_MOUNT="$TMP_DIR/rootfs"
STATIC_ARCHIVE="$TMP_DIR/xbps-static.tar.xz"
BUILD_DONE=0
trap cleanup EXIT
mkdir -p "$STATIC_DIR" "$ROOT_MOUNT"
log "downloading static XBPS from $STATIC_ARCHIVE_URL"
curl -fsSL "$STATIC_ARCHIVE_URL" -o "$STATIC_ARCHIVE"
tar -xf "$STATIC_ARCHIVE" -C "$STATIC_DIR"
XBPS_INSTALL="$(find_static_binary xbps-install)"
XBPS_QUERY="$(find_static_binary xbps-query)"
STATIC_KEYS_DIR="$(find_static_keys_dir)"
if [[ -z "$XBPS_INSTALL" || ! -x "$XBPS_INSTALL" ]]; then
log "failed to locate xbps-install in the static archive"
exit 1
fi
if [[ -z "$STATIC_KEYS_DIR" || ! -d "$STATIC_KEYS_DIR" ]]; then
log "failed to locate Void repository keys in the static archive"
exit 1
fi
log "creating $OUT_ROOTFS ($SIZE_SPEC)"
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
mkfs.ext4 -F -m 0 -L banger-void-root "$OUT_ROOTFS" >/dev/null
sudo mount -o loop "$OUT_ROOTFS" "$ROOT_MOUNT"
sudo mkdir -p "$ROOT_MOUNT/var/db/xbps/keys"
sudo cp -a "$STATIC_KEYS_DIR/." "$ROOT_MOUNT/var/db/xbps/keys/"
log "installing Void packages into the rootfs"
sudo env XBPS_ARCH="$ARCH" "$XBPS_INSTALL" -S -y -r "$ROOT_MOUNT" -R "$REPO_URL" "${VOID_PACKAGES[@]}"
if [[ -n "$XBPS_QUERY" && -x "$XBPS_QUERY" ]]; then
log "installed package set:"
sudo env XBPS_ARCH="$ARCH" "$XBPS_QUERY" -r "$ROOT_MOUNT" -l | awk '/^ii/ {print " " $2}' || true
fi
if [[ -n "$VOID_KERNEL_MODULES_DIR" ]]; then
log "copying staged Void kernel modules into the guest"
else
log "copying bundled kernel modules into the guest"
fi
sudo mkdir -p "$ROOT_MOUNT/lib/modules"
sudo cp -a "$MODULES_DIR" "$ROOT_MOUNT/lib/modules/"
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 and runit services"
install_guest_network_bootstrap
ensure_sshd_include
enable_sshd_service
install_vsock_service
configure_docker_bootstrap
enable_docker_service
normalize_root_shell
configure_root_bash_prompt
log "installing guest tools"
install_guest_tools
install_opencode_service
install_root_authorized_key
sudo touch "$ROOT_MOUNT/etc/fstab" "$ROOT_MOUNT/etc/hostname"
sudo chroot "$ROOT_MOUNT" /usr/bin/ssh-keygen -A
log "removing bulky caches, docs, and stale installer artifacts from the experimental image"
sudo rm -rf \
"$ROOT_MOUNT/var/cache/xbps" \
"$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/root/.cache/opencode" \
"$ROOT_MOUNT/tmp/get-docker" \
"$ROOT_MOUNT/tmp/get-docker.sh"
sudo rm -rf \
"$ROOT_MOUNT/root/.cache/mise" \
"$ROOT_MOUNT/root/.local/share/mise/downloads" \
"$ROOT_MOUNT/root/.local/share/mise/tmp"
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 Void rootfs: $OUT_ROOTFS"
log "built experimental Void work-seed: $WORK_SEED"
log "use examples/void.config.toml as the local config override template"

View file

@ -1,99 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[make-rootfs] %s\n' "$*"
}
usage() {
cat <<'EOF'
Usage: ./scripts/make-rootfs.sh --kernel <path> [--initrd <path>] [--modules <dir>] [--size <size>] [--base-rootfs <path>]
Builds build/manual/rootfs-docker.ext4 using scripts/customize.sh. If
--base-rootfs is omitted, the first existing file is used:
./build/manual/rootfs-base.ext4
./ubuntu-noble-rootfs/rootfs.ext4
./ubuntu-lts/rootfs.ext4
EOF
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MANUAL_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}"
OUT_ROOTFS="$MANUAL_DIR/rootfs-docker.ext4"
SIZE_SPEC="6G"
BASE_ROOTFS=""
KERNEL_PATH=""
INITRD_PATH=""
MODULES_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--size)
SIZE_SPEC="${2:-}"
shift 2
;;
--base-rootfs)
BASE_ROOTFS="${2:-}"
shift 2
;;
--kernel)
KERNEL_PATH="${2:-}"
shift 2
;;
--initrd)
INITRD_PATH="${2:-}"
shift 2
;;
--modules)
MODULES_DIR="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
log "unknown option: $1"
usage
exit 1
;;
esac
done
if [[ -z "$BASE_ROOTFS" ]]; then
if [[ -f "$MANUAL_DIR/rootfs-base.ext4" ]]; then
BASE_ROOTFS="$MANUAL_DIR/rootfs-base.ext4"
elif [[ -f "$REPO_ROOT/ubuntu-noble-rootfs/rootfs.ext4" ]]; then
BASE_ROOTFS="$REPO_ROOT/ubuntu-noble-rootfs/rootfs.ext4"
elif [[ -f "$REPO_ROOT/ubuntu-lts/rootfs.ext4" ]]; then
BASE_ROOTFS="$REPO_ROOT/ubuntu-lts/rootfs.ext4"
else
log "no base rootfs found; pass --base-rootfs"
exit 1
fi
fi
if [[ -z "$KERNEL_PATH" ]]; then
log "kernel path is required; pass --kernel"
exit 1
fi
mkdir -p "$MANUAL_DIR"
log "building $OUT_ROOTFS from $BASE_ROOTFS"
args=(
"$SCRIPT_DIR/customize.sh"
"$BASE_ROOTFS"
--out "$OUT_ROOTFS"
--size "$SIZE_SPEC"
--kernel "$KERNEL_PATH"
--docker
)
if [[ -n "$INITRD_PATH" ]]; then
args+=(--initrd "$INITRD_PATH")
fi
if [[ -n "$MODULES_DIR" ]]; then
args+=(--modules "$MODULES_DIR")
fi
exec "${args[@]}"

View file

@ -1,386 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[make-void-kernel] %s\n' "$*"
}
usage() {
cat <<'EOF'
Usage: ./scripts/make-void-kernel.sh [--out-dir <path>] [--mirror <url>] [--arch <arch>] [--kernel-package <name>] [--print-register-flags]
Download and stage a Void Linux kernel under ./build/manual/void-kernel for
the
experimental Void guest flow.
Defaults:
--out-dir ./build/manual/void-kernel
--mirror https://repo-default.voidlinux.org
--arch x86_64
--kernel-package linux6.12
The staged output contains:
boot/vmlinux-<version> Firecracker-usable kernel extracted from vmlinuz
boot/vmlinuz-<version> Raw distro boot image from the Void package
boot/initramfs-<version>.img Matching initramfs generated with dracut
boot/config-<version> Void kernel config
lib/modules/<version>/ Matching kernel modules tree
If --print-register-flags is passed, the script does not download anything. It
prints the banger image register flags for an existing staged Void kernel.
EOF
}
require_command() {
local name="$1"
command -v "$name" >/dev/null 2>&1 || {
log "required command not found: $name"
exit 1
}
}
normalize_mirror() {
local mirror="${1%/}"
mirror="${mirror%/current}"
mirror="${mirror%/static}"
printf '%s\n' "$mirror"
}
find_static_binary() {
local name="$1"
find "$STATIC_DIR" -type f \( -name "$name" -o -name "$name.static" \) -perm -u+x | sort | head -n 1
}
find_static_keys_dir() {
find "$STATIC_DIR" -type d -path '*/var/db/xbps/keys' | sort | head -n 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"
if [[ ! -d "$root" ]]; then
return 1
fi
find "$root" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1
}
print_register_flags() {
local kernel=""
local initrd=""
local modules=""
kernel="$(find_latest_matching "$OUT_DIR/boot" 'vmlinux-*' || true)"
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 Void 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"
}
check_elf() {
local path="$1"
readelf -h "$path" >/dev/null 2>&1
}
ensure_stage_root_layout() {
mkdir -p "$STAGE_ROOT/usr"
if [[ ! -e "$STAGE_ROOT/bin" ]]; then
ln -snf usr/bin "$STAGE_ROOT/bin"
fi
if [[ ! -e "$STAGE_ROOT/sbin" ]]; then
ln -snf usr/bin "$STAGE_ROOT/sbin"
fi
if [[ ! -e "$STAGE_ROOT/usr/sbin" ]]; then
ln -snf bin "$STAGE_ROOT/usr/sbin"
fi
if [[ ! -e "$STAGE_ROOT/lib" ]]; then
ln -snf usr/lib "$STAGE_ROOT/lib"
fi
if [[ ! -e "$STAGE_ROOT/lib64" ]]; then
ln -snf usr/lib "$STAGE_ROOT/lib64"
fi
if [[ ! -e "$STAGE_ROOT/usr/lib64" ]]; then
ln -snf lib "$STAGE_ROOT/usr/lib64"
fi
if [[ -x "$STAGE_ROOT/usr/bin/udevd" ]]; then
mkdir -p "$STAGE_ROOT/usr/lib/udev" "$STAGE_ROOT/usr/lib/systemd"
if [[ ! -e "$STAGE_ROOT/usr/lib/udev/udevd" ]]; then
ln -snf ../../bin/udevd "$STAGE_ROOT/usr/lib/udev/udevd"
fi
if [[ ! -e "$STAGE_ROOT/usr/lib/systemd/systemd-udevd" ]]; then
ln -snf ../../bin/udevd "$STAGE_ROOT/usr/lib/systemd/systemd-udevd"
fi
fi
}
sync_host_dracut_tree() {
if [[ ! -d /usr/lib/dracut ]]; then
log "host dracut support files not found under /usr/lib/dracut"
exit 1
fi
rm -rf "$STAGE_ROOT/usr/lib/dracut"
mkdir -p "$STAGE_ROOT/usr/lib"
cp -a /usr/lib/dracut "$STAGE_ROOT/usr/lib/dracut"
}
build_initramfs() {
local kver="$1"
local modules_dir="$2"
local out="$3"
local config_dir="$TMP_DIR/dracut.conf.d"
local tmpdir="$TMP_DIR/dracut-tmp"
local force_drivers="virtio virtio_ring virtio_mmio virtio_blk virtio_net virtio_console ext4 vsock vmw_vsock_virtio_transport"
mkdir -p "$config_dir" "$tmpdir"
ensure_stage_root_layout
sync_host_dracut_tree
log "generating initramfs for kernel $kver with host dracut against the staged Void sysroot"
env dracutbasedir="/usr/lib/dracut" dracut \
--force \
--kver "$kver" \
--sysroot "$STAGE_ROOT" \
--kmoddir "$modules_dir" \
--conf /dev/null \
--confdir "$config_dir" \
--tmpdir "$tmpdir" \
--no-hostonly \
--filesystems "ext4" \
--force-drivers "$force_drivers" \
--gzip \
"$out"
}
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
}
resolve_kernel_package_file() {
local escaped_name=""
escaped_name="$(printf '%s\n' "$KERNEL_PACKAGE" | sed 's/[.[\*^$()+?{|]/\\&/g')"
curl -fsSL "$REPO_URL/" |
grep -o "${escaped_name}-[0-9][^\" >]*\\.${ARCH}\\.xbps" |
sort -u |
tail -n 1
}
cleanup() {
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/void-kernel"
MIRROR="https://repo-default.voidlinux.org"
ARCH="x86_64"
KERNEL_PACKAGE="linux6.12"
PRINT_REGISTER_FLAGS=0
while [[ $# -gt 0 ]]; do
case "$1" in
--out-dir)
OUT_DIR="${2:-}"
shift 2
;;
--mirror)
MIRROR="${2:-}"
shift 2
;;
--arch)
ARCH="${2:-}"
shift 2
;;
--kernel-package)
KERNEL_PACKAGE="${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
MIRROR="$(normalize_mirror "$MIRROR")"
REPO_URL="$MIRROR/current"
STATIC_ARCHIVE_URL="$MIRROR/static/xbps-static-latest.x86_64-musl.tar.xz"
if [[ "$PRINT_REGISTER_FLAGS" == "1" ]]; then
print_register_flags
exit 0
fi
if [[ "$ARCH" != "x86_64" ]]; then
log "unsupported arch: $ARCH"
log "this experimental downloader currently supports only x86_64"
exit 1
fi
mkdir -p "$(dirname "$OUT_DIR")"
if [[ -e "$OUT_DIR" ]]; then
log "output directory already exists: $OUT_DIR"
log "remove it first if you want to re-stage a different Void kernel"
exit 1
fi
require_command curl
require_command tar
require_command cp
require_command find
require_command grep
require_command cut
require_command readelf
require_command file
require_command install
require_command tail
require_command xz
require_command gzip
require_command bzip2
require_command dracut
TMP_DIR="$(mktemp -d -t banger-void-kernel-XXXXXX)"
STATIC_DIR="$TMP_DIR/static"
STAGE_ROOT="$TMP_DIR/root"
STAGE_OUT="$TMP_DIR/out"
STATIC_ARCHIVE="$TMP_DIR/xbps-static.tar.xz"
trap cleanup EXIT
mkdir -p "$STATIC_DIR" "$STAGE_ROOT/var/db/xbps/keys" "$STAGE_OUT/boot" "$STAGE_OUT/lib/modules"
log "downloading static XBPS from $STATIC_ARCHIVE_URL"
curl -fsSL "$STATIC_ARCHIVE_URL" -o "$STATIC_ARCHIVE"
tar -xf "$STATIC_ARCHIVE" -C "$STATIC_DIR"
XBPS_INSTALL="$(find_static_binary xbps-install)"
STATIC_KEYS_DIR="$(find_static_keys_dir)"
if [[ -z "$XBPS_INSTALL" || ! -x "$XBPS_INSTALL" ]]; then
log "failed to locate xbps-install in the static archive"
exit 1
fi
if [[ -z "$STATIC_KEYS_DIR" || ! -d "$STATIC_KEYS_DIR" ]]; then
log "failed to locate Void repository keys in the static archive"
exit 1
fi
cp -a "$STATIC_KEYS_DIR/." "$STAGE_ROOT/var/db/xbps/keys/"
KERNEL_PACKAGE_FILE="$(resolve_kernel_package_file)"
if [[ -z "$KERNEL_PACKAGE_FILE" ]]; then
log "failed to resolve a package file for $KERNEL_PACKAGE in $REPO_URL"
exit 1
fi
log "staging $KERNEL_PACKAGE_FILE into a temporary root"
env XBPS_ARCH="$ARCH" "$XBPS_INSTALL" -S -y -U -r "$STAGE_ROOT" -R "$REPO_URL" linux-base "$KERNEL_PACKAGE" dracut eudev >/dev/null
VMLINUX_RAW="$(find_latest_matching "$STAGE_ROOT/boot" 'vmlinuz-*' || true)"
KERNEL_CONFIG="$(find_latest_matching "$STAGE_ROOT/boot" 'config-*' || true)"
MODULES_DIR="$(find_latest_module_dir "$STAGE_ROOT/usr/lib/modules" || true)"
KERNEL_VERSION="$(basename "$MODULES_DIR")"
INITRAMFS_NAME="initramfs-${KERNEL_VERSION}.img"
INITRAMFS_RAW="$STAGE_OUT/boot/$INITRAMFS_NAME"
if [[ -z "$VMLINUX_RAW" || -z "$KERNEL_CONFIG" || -z "$MODULES_DIR" ]]; then
log "staged Void kernel is missing expected boot artifacts"
exit 1
fi
if [[ ! -x "$STAGE_ROOT/usr/bin/udevd" ]]; then
log "staged Void sysroot is missing /usr/bin/udevd after package install"
exit 1
fi
VMLINUX_BASE="$(basename "$VMLINUX_RAW")"
VMLINUX_OUT="$STAGE_OUT/boot/vmlinux-${VMLINUX_BASE#vmlinuz-}"
install -m 0644 "$VMLINUX_RAW" "$STAGE_OUT/boot/$VMLINUX_BASE"
install -m 0644 "$KERNEL_CONFIG" "$STAGE_OUT/boot/$(basename "$KERNEL_CONFIG")"
build_initramfs "$KERNEL_VERSION" "$MODULES_DIR" "$INITRAMFS_RAW"
cp -a "$MODULES_DIR" "$STAGE_OUT/lib/modules/"
log "extracting Firecracker kernel from $(basename "$VMLINUX_RAW")"
if ! extract_vmlinux "$VMLINUX_RAW" "$VMLINUX_OUT"; then
log "failed to extract an uncompressed vmlinux from $VMLINUX_RAW"
log "raw kernel image type: $(file -b "$VMLINUX_RAW")"
exit 1
fi
cat >"$STAGE_OUT/metadata.json" <<EOF
{
"package": "$KERNEL_PACKAGE_FILE",
"kernel_path": "$OUT_DIR/boot/$(basename "$VMLINUX_OUT")",
"raw_kernel_path": "$OUT_DIR/boot/$VMLINUX_BASE",
"config_path": "$OUT_DIR/boot/$(basename "$KERNEL_CONFIG")",
"initrd_path": "$OUT_DIR/boot/$INITRAMFS_NAME",
"modules_dir": "$OUT_DIR/lib/modules/$(basename "$MODULES_DIR")"
}
EOF
mv "$STAGE_OUT" "$OUT_DIR"
log "staged Void kernel artifacts in $OUT_DIR"
log "kernel image: $OUT_DIR/boot/$(basename "$VMLINUX_OUT")"
log "initrd image: $OUT_DIR/boot/$INITRAMFS_NAME"
log "modules dir: $OUT_DIR/lib/modules/$(basename "$MODULES_DIR")"

View file

@ -1,64 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[register-alpine-image] %s\n' "$*" >&2
}
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}"
KERNEL_REF="${ALPINE_KERNEL_REF:-$IMAGE_NAME}"
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
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
log "importing Alpine kernel from $RUNTIME_DIR/alpine-kernel as $KERNEL_REF"
"$BANGER_BIN" kernel import "$KERNEL_REF" \
--from "$RUNTIME_DIR/alpine-kernel" \
--distro alpine \
--arch x86_64
log "registering image $IMAGE_NAME with kernel-ref $KERNEL_REF"
"$BANGER_BIN" image register \
--name "$IMAGE_NAME" \
--rootfs "$ROOTFS" \
--work-seed "$WORK_SEED" \
--docker \
--kernel-ref "$KERNEL_REF"

View file

@ -1,63 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[register-void-image] %s\n' "$*" >&2
}
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="${VOID_IMAGE_NAME:-void}"
KERNEL_REF="${VOID_KERNEL_REF:-$IMAGE_NAME}"
BANGER_BIN="$(resolve_banger_bin)"
ROOTFS="$RUNTIME_DIR/rootfs-void.ext4"
WORK_SEED="$RUNTIME_DIR/rootfs-void.work-seed.ext4"
if [[ ! -f "$ROOTFS" ]]; then
log "missing Void rootfs: $ROOTFS"
exit 1
fi
if [[ ! -f "$WORK_SEED" ]]; then
log "missing Void work-seed: $WORK_SEED"
exit 1
fi
if [[ ! -d "$RUNTIME_DIR/void-kernel" ]]; then
log "missing staged Void kernel artifacts: $RUNTIME_DIR/void-kernel"
log "run 'make void-kernel' before registering $IMAGE_NAME"
exit 1
fi
log "importing Void kernel from $RUNTIME_DIR/void-kernel as $KERNEL_REF"
"$BANGER_BIN" kernel import "$KERNEL_REF" \
--from "$RUNTIME_DIR/void-kernel" \
--distro void \
--arch x86_64
log "registering image $IMAGE_NAME with kernel-ref $KERNEL_REF"
"$BANGER_BIN" image register \
--name "$IMAGE_NAME" \
--rootfs "$ROOTFS" \
--work-seed "$WORK_SEED" \
--kernel-ref "$KERNEL_REF"

View file

@ -1,334 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[verify] %s\n' "$*"
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DAEMON_LOG="${XDG_STATE_HOME:-$HOME/.local/state}/banger/bangerd.log"
OPENCODE_PORT=4096
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; run 'make build' or set BANGER_BIN"
exit 1
}
BANGER_BIN="$(resolve_banger_bin)"
SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)"
if [[ ! -f "$SSH_KEY" ]]; then
log "ssh key not found: $SSH_KEY"
exit 1
fi
SSH_COMMON_ARGS=(
-F /dev/null
-i "$SSH_KEY"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o PreferredAuthentications=publickey
-o PasswordAuthentication=no
-o KbdInteractiveAuthentication=no
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
)
firecracker_running() {
local pid="$1"
local api_sock="$2"
local cmdline=""
if [[ -z "$pid" || "$pid" -le 0 || -z "$api_sock" ]]; then
return 1
fi
if [[ ! -r "/proc/$pid/cmdline" ]]; then
return 1
fi
cmdline="$(cat "/proc/$pid/cmdline" 2>/dev/null | tr '\0' ' ' || true)"
[[ "$cmdline" == *firecracker* && "$cmdline" == *"$api_sock"* ]]
}
pooled_tap() {
local tap="$1"
[[ "$tap" == tap-pool-* ]]
}
wait_for_ssh() {
local guest_ip="$1"
local deadline="$2"
while ((SECONDS < deadline)); do
if ssh "${SSH_COMMON_ARGS[@]}" -o ConnectTimeout=2 "root@${guest_ip}" "true" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
wait_for_tcp() {
local host="$1"
local port="$2"
local deadline="$3"
while ((SECONDS < deadline)); do
if (exec 3<>/dev/tcp/"$host"/"$port") >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
refresh_vm_metadata() {
if ! VM_JSON="$("$BANGER_BIN" vm show "$VM_NAME" 2>/dev/null)"; then
return 1
fi
TAP="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.tap_device // empty')"
VM_DIR="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.vm_dir // empty')"
GUEST_IP="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.guest_ip // empty')"
API_SOCK="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.api_sock_path // empty')"
PID="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.pid // 0')"
VM_STATE="$(printf '%s\n' "$VM_JSON" | jq -r '.state // empty')"
LAST_ERROR="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.last_error // empty')"
return 0
}
wait_for_vm_ready() {
local deadline="$1"
while ((SECONDS < deadline)); do
if ! refresh_vm_metadata; then
sleep 1
continue
fi
if [[ "$VM_STATE" == "error" || -n "$LAST_ERROR" ]]; then
return 2
fi
if [[ -n "$API_SOCK" && "${PID:-0}" -gt 0 ]] && ! firecracker_running "$PID" "$API_SOCK"; then
return 3
fi
if [[ "$VM_STATE" == "running" && -n "$GUEST_IP" && -n "$TAP" && -n "$VM_DIR" && -n "$API_SOCK" && "${PID:-0}" -gt 0 ]]; then
if [[ -S "$API_SOCK" ]] && ip link show "$TAP" >/dev/null 2>&1; then
return 0
fi
fi
sleep 1
done
return 1
}
dump_diagnostics() {
log "diagnostics for $VM_NAME"
"$BANGER_BIN" vm show "$VM_NAME" || true
if [[ "${PID:-0}" -gt 0 ]]; then
log "process state for pid $PID"
ps -fp "$PID" || true
fi
log "recent firecracker log"
"$BANGER_BIN" vm logs "$VM_NAME" 2>/dev/null | tail -n 200 || true
if [[ -f "$DAEMON_LOG" ]]; then
log "recent daemon log"
tail -n 200 "$DAEMON_LOG" || true
fi
if [[ -n "${TAP:-}" ]]; then
log "tap state for $TAP"
ip link show "$TAP" || true
fi
if [[ -n "${API_SOCK:-}" ]]; then
log "api socket $API_SOCK"
ls -l "$API_SOCK" 2>/dev/null || true
fi
if (( NAT_ENABLED )) && [[ -n "${UPLINK:-}" && -n "${GUEST_IP:-}" && -n "${TAP:-}" ]]; then
log "nat rules for ${GUEST_IP} via ${UPLINK}"
sudo iptables -t nat -S POSTROUTING | grep "${GUEST_IP}/32" || true
sudo iptables -S FORWARD | grep "$TAP" || true
fi
}
usage() {
cat <<'EOF'
Usage: ./scripts/verify.sh [--nat] [--image <name>]
Run a basic smoke test for the Go VM workflow.
Use --nat to additionally verify outbound NAT and host rule cleanup.
Use --image to verify a non-default image such as void.
EOF
}
NAT_ENABLED=0
IMAGE_NAME=""
BOOT_TIMEOUT_SECS="${VERIFY_BOOT_TIMEOUT_SECS:-90}"
while [[ $# -gt 0 ]]; do
case "$1" in
--nat)
NAT_ENABLED=1
shift
;;
--image)
IMAGE_NAME="${2:-}"
if [[ -z "$IMAGE_NAME" ]]; then
usage
exit 1
fi
shift 2
;;
*)
usage
exit 1
;;
esac
done
VM_NAME="verify-$(date +%s)"
VM_JSON=""
TAP=""
VM_DIR=""
GUEST_IP=""
UPLINK=""
API_SOCK=""
PID="0"
VM_STATE=""
LAST_ERROR=""
delete_vm() {
if [[ -n "${VM_NAME:-}" ]]; then
"$BANGER_BIN" vm delete "$VM_NAME"
fi
}
cleanup() {
if [[ -n "${VM_NAME:-}" ]]; then
"$BANGER_BIN" vm delete "$VM_NAME" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
log "starting VM"
CREATE_ARGS=("$BANGER_BIN" vm create --name "$VM_NAME")
if [[ -n "$IMAGE_NAME" ]]; then
CREATE_ARGS+=(--image "$IMAGE_NAME")
fi
if (( NAT_ENABLED )); then
CREATE_ARGS+=(--nat)
fi
"${CREATE_ARGS[@]}" >/dev/null
BOOT_DEADLINE=$((SECONDS + BOOT_TIMEOUT_SECS))
log "waiting for VM runtime readiness"
if wait_for_vm_ready "$BOOT_DEADLINE"; then
:
else
status=$?
case "$status" in
2) log "vm entered an error state before becoming ready" ;;
3) log "firecracker exited before the guest became ready" ;;
*) log "vm did not become ready before timeout" ;;
esac
dump_diagnostics
exit 1
fi
if (( NAT_ENABLED )); then
UPLINK="$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')"
if [[ -z "$UPLINK" ]]; then
log "failed to detect uplink interface"
exit 1
fi
log "asserting NAT rules are installed"
sudo iptables -t nat -C POSTROUTING -s "${GUEST_IP}/32" -o "$UPLINK" -j MASQUERADE
sudo iptables -C FORWARD -i "$TAP" -o "$UPLINK" -j ACCEPT
sudo iptables -C FORWARD -i "$UPLINK" -o "$TAP" -m state --state RELATED,ESTABLISHED -j ACCEPT
fi
log "asserting VM is reachable via SSH"
if ! wait_for_ssh "$GUEST_IP" "$BOOT_DEADLINE"; then
log "ssh did not become ready for ${GUEST_IP}"
dump_diagnostics
exit 1
fi
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "uname -a" >/dev/null
log "asserting opencode is available and listening in the guest"
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "command -v opencode >/dev/null 2>&1 && ss -H -lntp | awk '\$4 ~ /:${OPENCODE_PORT}\$/ { found = 1 } END { exit found ? 0 : 1 }'" >/dev/null
log "asserting opencode server is reachable from the host"
if ! wait_for_tcp "$GUEST_IP" "$OPENCODE_PORT" "$BOOT_DEADLINE"; then
log "opencode server did not become reachable at ${GUEST_IP}:${OPENCODE_PORT}"
dump_diagnostics
exit 1
fi
log "asserting opencode port is reported by banger vm ports"
if ! "$BANGER_BIN" vm ports "$VM_NAME" | grep -F ":${OPENCODE_PORT}" >/dev/null 2>&1; then
log "banger vm ports did not report ${OPENCODE_PORT}"
dump_diagnostics
exit 1
fi
if (( NAT_ENABLED )); then
log "asserting VM has outbound network access"
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "curl -fsS https://example.com >/dev/null" >/dev/null
fi
log "cleaning up VM"
if ! delete_vm; then
log "vm delete failed for $VM_NAME"
dump_diagnostics
exit 1
fi
log "asserting cleanup success"
if "$BANGER_BIN" vm show "$VM_NAME" >/dev/null 2>&1; then
log "vm still exists after delete: $VM_NAME"
exit 1
fi
if ip link show "$TAP" >/dev/null 2>&1; then
if pooled_tap "$TAP"; then
log "tap returned to idle pool: $TAP"
else
log "tap still exists: $TAP"
exit 1
fi
fi
if [[ -d "$VM_DIR" ]]; then
log "vm dir still exists: $VM_DIR"
exit 1
fi
if (( NAT_ENABLED )); then
if sudo iptables -t nat -C POSTROUTING -s "${GUEST_IP}/32" -o "$UPLINK" -j MASQUERADE 2>/dev/null; then
log "nat rule still exists for ${GUEST_IP}"
exit 1
fi
if sudo iptables -C FORWARD -i "$TAP" -o "$UPLINK" -j ACCEPT 2>/dev/null; then
log "forward-out rule still exists for ${TAP}"
exit 1
fi
if sudo iptables -C FORWARD -i "$UPLINK" -o "$TAP" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
log "forward-in rule still exists for ${TAP}"
exit 1
fi
fi
log "ok"

15
todos Normal file
View file

@ -0,0 +1,15 @@
when developing, vm creation may fail, and firecracker logs need to be manually looked into, we should add a convenient way of digging through it. or perhaps log the last lines of it when vm creation fails
`banger vm run` can hang waiting for ssh if things go south with sshd inside the vm. I think we should have a timeout instead of hanging forever. And also log what the user can do in such scenario.
some commands are expected to take a while, it'd be good to at least show an indicator that banger is not hanging but rather doing something expected
perhaps add an "interactive flag"?
my computer is not the usual computer that users may have. perhaps it would be a good idea to screen the hardware we're working on so that we can set reasonable defaults for people when installing?
versioning and releasing could use some love
coverage would be somewhat nice to have
regular users have no idea how to point their machine DNS to use the banger dns server. they need a tutorial/docs for this