#!/usr/bin/env bash set -euo pipefail log() { printf '[make-rootfs-void] %s\n' "$*" } usage() { cat <<'EOF' Usage: ./make-rootfs-void.sh [--out ] [--size ] [--mirror ] [--arch ] [--packages ] Build an experimental Void Linux rootfs image plus a matching /root work-seed. Defaults: --out ./runtime/rootfs-void.ext4 --size 2G --mirror https://repo-default.voidlinux.org --arch x86_64 --packages ./packages.void This path is experimental and local-only. It reuses the current runtime bundle kernel/initrd/modules and 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 "$SCRIPT_DIR/banger" ]]; then printf '%s\n' "$SCRIPT_DIR/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" } bundle_path() { local key="$1" local fallback="$2" local rel="" if [[ -f "$BUNDLE_METADATA" ]] && command -v jq >/dev/null 2>&1; then rel="$(jq -r --arg key "$key" '.[$key] // empty' "$BUNDLE_METADATA" 2>/dev/null || true)" fi if [[ -n "$rel" && "$rel" != "null" ]]; then printf '%s\n' "$RUNTIME_DIR/$rel" return fi printf '%s\n' "$fallback" } 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 } 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" } 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:-}" 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 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" } 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)" PACKAGES_FILE="$SCRIPT_DIR/packages.void" export BANGER_APT_PACKAGES_FILE="$PACKAGES_FILE" source "$SCRIPT_DIR/packages.sh" DEFAULT_RUNTIME_DIR="$SCRIPT_DIR" if [[ -d "$SCRIPT_DIR/runtime" ]]; then DEFAULT_RUNTIME_DIR="$SCRIPT_DIR/runtime" fi RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}" if [[ ! -d "$RUNTIME_DIR" ]]; then log "runtime bundle not found: $RUNTIME_DIR" log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR" exit 1 fi BUNDLE_METADATA="$RUNTIME_DIR/bundle.json" OUT_ROOTFS="$RUNTIME_DIR/rootfs-void.ext4" SIZE_SPEC="2G" MIRROR="https://repo-default.voidlinux.org" ARCH="x86_64" MODULES_DIR="$(bundle_path default_modules_dir "$RUNTIME_DIR/wtf/root/lib/modules/6.8.0-94-generic")" VSOCK_AGENT="$(bundle_path vsock_agent_path "$RUNTIME_DIR/banger-vsock-agent")" if [[ "$VSOCK_AGENT" == "$RUNTIME_DIR/banger-vsock-agent" && ! -x "$VSOCK_AGENT" ]]; then VSOCK_AGENT="$(bundle_path vsock_ping_helper_path "$RUNTIME_DIR/banger-vsock-pingd")" 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 ;; --packages) PACKAGES_FILE="${2:-}" export BANGER_APT_PACKAGES_FILE="$PACKAGES_FILE" 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 [[ ! -f "$PACKAGES_FILE" ]]; then log "package manifest not found: $PACKAGES_FILE" exit 1 fi if [[ ! -d "$MODULES_DIR" ]]; then log "modules dir not found: $MODULES_DIR" exit 1 fi if [[ ! -x "$VSOCK_AGENT" ]]; then log "vsock agent not found or not executable: $VSOCK_AGENT" log "run 'make build' or refresh the runtime bundle" 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 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 ! banger_packages_read_array VOID_PACKAGES "$PACKAGES_FILE"; then log "package manifest is empty: $PACKAGES_FILE" exit 1 fi if ! PACKAGES_HASH="$(banger_packages_manifest_hash "$PACKAGES_FILE")"; then log "failed to hash package manifest: $PACKAGES_FILE" exit 1 fi if ! SIZE_BYTES="$(parse_size "$SIZE_SPEC")"; then log "invalid size: $SIZE_SPEC" exit 1 fi BANGER_BIN="$(resolve_banger_bin)" 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 log "copying bundled kernel modules into the guest" 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" ensure_sshd_include enable_sshd_service install_vsock_service configure_docker_bootstrap enable_docker_service normalize_root_shell configure_root_bash_prompt sudo mkdir -p "$ROOT_MOUNT/root/.ssh" 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/tmp/get-docker" \ "$ROOT_MOUNT/tmp/get-docker.sh" sudo umount "$ROOT_MOUNT" banger_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-exp.config.toml as the local config override template"