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